moflo 4.12.4-rc.1 → 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/helpers/gate-hook.mjs +34 -0
- package/.claude/helpers/gate.cjs +110 -6
- package/.claude/helpers/hook-handler.cjs +0 -0
- package/.claude/helpers/prompt-hook.mjs +0 -0
- package/bin/gate-hook.mjs +34 -0
- package/bin/gate.cjs +110 -6
- package/dist/src/cli/commands/agent.js +1 -89
- package/dist/src/cli/commands/benchmark.js +11 -8
- package/dist/src/cli/commands/config.js +256 -96
- package/dist/src/cli/commands/deployment.js +4 -4
- package/dist/src/cli/commands/doctor-checks-config.js +18 -22
- package/dist/src/cli/commands/doctor-fixes.js +36 -11
- package/dist/src/cli/commands/embeddings.js +62 -21
- 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 +19 -19
- package/dist/src/cli/commands/performance.js +12 -5
- package/dist/src/cli/commands/security.js +2 -2
- 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 +41 -31
- package/dist/src/cli/init/claudemd-generator.js +3 -3
- package/dist/src/cli/init/helpers-generator.js +165 -15
- package/dist/src/cli/mcp-client.js +12 -2
- 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/memory-admin-tools.js +538 -0
- package/dist/src/cli/mcp-tools/memory-tools.js +1 -1
- package/dist/src/cli/mcp-tools/neural-tools.js +53 -105
- package/dist/src/cli/mcp-tools/performance-tools.js +72 -53
- package/dist/src/cli/mcp-tools/progress-tools.js +142 -0
- package/dist/src/cli/mcp-tools/session-tools.js +116 -0
- package/dist/src/cli/mcp-tools/system-tools.js +127 -94
- 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/parser.js +81 -12
- package/dist/src/cli/services/spell-gate.js +5 -3
- 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/version.js +1 -1
- package/package.json +2 -2
- package/dist/src/cli/mcp-tools/github-tools.js +0 -331
|
@@ -236,10 +236,9 @@ mcp__moflo__performance_benchmark {
|
|
|
236
236
|
iterations: 10
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
//
|
|
239
|
+
// Read this process's CPU / memory / heap usage
|
|
240
240
|
mcp__moflo__performance_report {
|
|
241
|
-
|
|
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
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
//
|
|
314
|
-
|
|
315
|
-
|
|
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
|
|
|
@@ -71,6 +71,40 @@ if (hookContext.tool_input && typeof hookContext.tool_input === 'object') {
|
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
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
|
+
|
|
74
108
|
// Run gate.cjs with the enriched environment
|
|
75
109
|
var projectDir = (env.CLAUDE_PROJECT_DIR || process.cwd()).replace(/^\/([a-z])\//i, '$1:/');
|
|
76
110
|
var gateScript = resolve(projectDir, '.claude/helpers/gate.cjs');
|
package/.claude/helpers/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
|
}
|
|
@@ -902,6 +975,17 @@ switch (command) {
|
|
|
902
975
|
overall = 'UNVERIFIED';
|
|
903
976
|
}
|
|
904
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
|
+
}
|
|
905
989
|
vs.verifyOutcome = overall;
|
|
906
990
|
writeState(vs);
|
|
907
991
|
break;
|
|
@@ -912,6 +996,17 @@ switch (command) {
|
|
|
912
996
|
// (.github/workflows/, .github/ISSUE_TEMPLATE/, .github/PULL_REQUEST_TEMPLATE/, #1176):
|
|
913
997
|
// no gate reset — editing these doesn't expose new runtime surface.
|
|
914
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;
|
|
915
1010
|
var s = readState();
|
|
916
1011
|
// Test-only edits invalidate testsRun but preserve simplifyRun (#908).
|
|
917
1012
|
var isTestOnly = fp && EDIT_RESET_SKIP_SIMPLIFY_ONLY_RE.test(fp);
|
|
@@ -1018,7 +1113,7 @@ switch (command) {
|
|
|
1018
1113
|
}
|
|
1019
1114
|
}
|
|
1020
1115
|
var missing = [];
|
|
1021
|
-
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)');
|
|
1022
1117
|
if (config.simplify_gate && !s.simplifyRun) missing.push('/flo-simplify (or /distill) has not run since the last code edit');
|
|
1023
1118
|
if (config.learnings_gate && !s.learningsStored) missing.push('learnings have not been stored (call mcp__moflo__memory_store)');
|
|
1024
1119
|
if (missing.length === 0) break;
|
|
@@ -1029,6 +1124,10 @@ switch (command) {
|
|
|
1029
1124
|
if (s.lastResetBy && s.lastResetBy.file) {
|
|
1030
1125
|
process.stderr.write('Last gate reset: ' + s.lastResetBy.file + ' (' + (s.lastResetBy.gates || []).join(', ') + ')\n');
|
|
1031
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);
|
|
1032
1131
|
process.stderr.write('Disable per-gate via moflo.yaml:\n');
|
|
1033
1132
|
process.stderr.write(' gates:\n testing_gate: false\n simplify_gate: false\n learnings_gate: false\n');
|
|
1034
1133
|
process.exit(2);
|
|
@@ -1079,7 +1178,12 @@ switch (command) {
|
|
|
1079
1178
|
// recorded a structured outcome (interrupted, or it stored prose only).
|
|
1080
1179
|
process.stderr.write(' - /verify ran but recorded no verdict — re-run it so it stores a structured result\n');
|
|
1081
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');
|
|
1082
1185
|
}
|
|
1186
|
+
process.stderr.write(ORDER_HINT);
|
|
1083
1187
|
process.stderr.write('Disable via moflo.yaml:\n');
|
|
1084
1188
|
process.stderr.write(' gates:\n verify_before_done: false\n');
|
|
1085
1189
|
process.exit(2);
|
|
File without changes
|
|
File without changes
|
package/bin/gate-hook.mjs
CHANGED
|
@@ -71,6 +71,40 @@ if (hookContext.tool_input && typeof hookContext.tool_input === 'object') {
|
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
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
|
+
|
|
74
108
|
// Run gate.cjs with the enriched environment
|
|
75
109
|
var projectDir = (env.CLAUDE_PROJECT_DIR || process.cwd()).replace(/^\/([a-z])\//i, '$1:/');
|
|
76
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
|
}
|
|
@@ -902,6 +975,17 @@ switch (command) {
|
|
|
902
975
|
overall = 'UNVERIFIED';
|
|
903
976
|
}
|
|
904
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
|
+
}
|
|
905
989
|
vs.verifyOutcome = overall;
|
|
906
990
|
writeState(vs);
|
|
907
991
|
break;
|
|
@@ -912,6 +996,17 @@ switch (command) {
|
|
|
912
996
|
// (.github/workflows/, .github/ISSUE_TEMPLATE/, .github/PULL_REQUEST_TEMPLATE/, #1176):
|
|
913
997
|
// no gate reset — editing these doesn't expose new runtime surface.
|
|
914
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;
|
|
915
1010
|
var s = readState();
|
|
916
1011
|
// Test-only edits invalidate testsRun but preserve simplifyRun (#908).
|
|
917
1012
|
var isTestOnly = fp && EDIT_RESET_SKIP_SIMPLIFY_ONLY_RE.test(fp);
|
|
@@ -1018,7 +1113,7 @@ switch (command) {
|
|
|
1018
1113
|
}
|
|
1019
1114
|
}
|
|
1020
1115
|
var missing = [];
|
|
1021
|
-
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)');
|
|
1022
1117
|
if (config.simplify_gate && !s.simplifyRun) missing.push('/flo-simplify (or /distill) has not run since the last code edit');
|
|
1023
1118
|
if (config.learnings_gate && !s.learningsStored) missing.push('learnings have not been stored (call mcp__moflo__memory_store)');
|
|
1024
1119
|
if (missing.length === 0) break;
|
|
@@ -1029,6 +1124,10 @@ switch (command) {
|
|
|
1029
1124
|
if (s.lastResetBy && s.lastResetBy.file) {
|
|
1030
1125
|
process.stderr.write('Last gate reset: ' + s.lastResetBy.file + ' (' + (s.lastResetBy.gates || []).join(', ') + ')\n');
|
|
1031
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);
|
|
1032
1131
|
process.stderr.write('Disable per-gate via moflo.yaml:\n');
|
|
1033
1132
|
process.stderr.write(' gates:\n testing_gate: false\n simplify_gate: false\n learnings_gate: false\n');
|
|
1034
1133
|
process.exit(2);
|
|
@@ -1079,7 +1178,12 @@ switch (command) {
|
|
|
1079
1178
|
// recorded a structured outcome (interrupted, or it stored prose only).
|
|
1080
1179
|
process.stderr.write(' - /verify ran but recorded no verdict — re-run it so it stores a structured result\n');
|
|
1081
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');
|
|
1082
1185
|
}
|
|
1186
|
+
process.stderr.write(ORDER_HINT);
|
|
1083
1187
|
process.stderr.write('Disable via moflo.yaml:\n');
|
|
1084
1188
|
process.stderr.write(' gates:\n verify_before_done: false\n');
|
|
1085
1189
|
process.exit(2);
|
|
@@ -690,94 +690,6 @@ const healthCommand = {
|
|
|
690
690
|
}
|
|
691
691
|
}
|
|
692
692
|
};
|
|
693
|
-
// Agent logs subcommand
|
|
694
|
-
const logsCommand = {
|
|
695
|
-
name: 'logs',
|
|
696
|
-
description: 'Show agent activity logs',
|
|
697
|
-
options: [
|
|
698
|
-
{
|
|
699
|
-
name: 'id',
|
|
700
|
-
short: 'i',
|
|
701
|
-
description: 'Agent ID',
|
|
702
|
-
type: 'string'
|
|
703
|
-
},
|
|
704
|
-
{
|
|
705
|
-
name: 'tail',
|
|
706
|
-
short: 'n',
|
|
707
|
-
description: 'Number of recent entries',
|
|
708
|
-
type: 'number',
|
|
709
|
-
default: 50
|
|
710
|
-
},
|
|
711
|
-
{
|
|
712
|
-
name: 'level',
|
|
713
|
-
short: 'l',
|
|
714
|
-
description: 'Minimum log level',
|
|
715
|
-
type: 'string',
|
|
716
|
-
choices: ['debug', 'info', 'warn', 'error'],
|
|
717
|
-
default: 'info'
|
|
718
|
-
},
|
|
719
|
-
{
|
|
720
|
-
name: 'follow',
|
|
721
|
-
short: 'f',
|
|
722
|
-
description: 'Follow log output',
|
|
723
|
-
type: 'boolean',
|
|
724
|
-
default: false
|
|
725
|
-
},
|
|
726
|
-
{
|
|
727
|
-
name: 'since',
|
|
728
|
-
description: 'Show logs since (e.g., "1h", "30m")',
|
|
729
|
-
type: 'string'
|
|
730
|
-
}
|
|
731
|
-
],
|
|
732
|
-
examples: [
|
|
733
|
-
{ command: 'flo agent logs -i agent-001', description: 'Show agent logs' },
|
|
734
|
-
{ command: 'flo agent logs -i agent-001 -f', description: 'Follow agent logs' },
|
|
735
|
-
{ command: 'flo agent logs -l error --since 1h', description: 'Show errors from last hour' }
|
|
736
|
-
],
|
|
737
|
-
action: async (ctx) => {
|
|
738
|
-
const agentId = ctx.args[0] || ctx.flags.id;
|
|
739
|
-
const tail = ctx.flags.tail;
|
|
740
|
-
const level = ctx.flags.level;
|
|
741
|
-
if (!agentId) {
|
|
742
|
-
output.printError('Agent ID is required. Use --id or -i');
|
|
743
|
-
return { success: false, exitCode: 1 };
|
|
744
|
-
}
|
|
745
|
-
try {
|
|
746
|
-
const result = await callMCPTool('agent_logs', {
|
|
747
|
-
agentId,
|
|
748
|
-
tail,
|
|
749
|
-
level,
|
|
750
|
-
since: ctx.flags.since,
|
|
751
|
-
});
|
|
752
|
-
if (ctx.flags.format === 'json') {
|
|
753
|
-
output.printJson(result);
|
|
754
|
-
return { success: true, data: result };
|
|
755
|
-
}
|
|
756
|
-
output.writeln();
|
|
757
|
-
output.writeln(output.bold(`Logs for ${agentId}`));
|
|
758
|
-
output.writeln(output.dim(`Showing ${result.entries.length} of ${result.total} entries`));
|
|
759
|
-
output.writeln();
|
|
760
|
-
for (const entry of result.entries) {
|
|
761
|
-
const time = new Date(entry.timestamp).toLocaleTimeString();
|
|
762
|
-
const levelStr = formatLogLevel(entry.level);
|
|
763
|
-
output.writeln(`${output.dim(time)} ${levelStr} ${entry.message}`);
|
|
764
|
-
if (entry.context && Object.keys(entry.context).length > 0) {
|
|
765
|
-
output.writeln(output.dim(` ${JSON.stringify(entry.context)}`));
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
return { success: true, data: result };
|
|
769
|
-
}
|
|
770
|
-
catch (error) {
|
|
771
|
-
if (error instanceof MCPClientError) {
|
|
772
|
-
output.printError(`Logs error: ${error.message}`);
|
|
773
|
-
}
|
|
774
|
-
else {
|
|
775
|
-
output.printError(`Unexpected error: ${String(error)}`);
|
|
776
|
-
}
|
|
777
|
-
return { success: false, exitCode: 1 };
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
};
|
|
781
693
|
function formatLogLevel(level) {
|
|
782
694
|
switch (level) {
|
|
783
695
|
case 'debug':
|
|
@@ -796,7 +708,7 @@ function formatLogLevel(level) {
|
|
|
796
708
|
export const agentCommand = {
|
|
797
709
|
name: 'agent',
|
|
798
710
|
description: 'Agent management commands',
|
|
799
|
-
subcommands: [spawnCommand, listCommand, statusCommand, stopCommand, metricsCommand, poolCommand, healthCommand
|
|
711
|
+
subcommands: [spawnCommand, listCommand, statusCommand, stopCommand, metricsCommand, poolCommand, healthCommand],
|
|
800
712
|
options: [],
|
|
801
713
|
examples: [
|
|
802
714
|
{ command: 'flo agent spawn -t coder', description: 'Spawn a coder agent' },
|