moflo 4.12.7 → 4.12.9
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/helpers/gate-hook.mjs +16 -0
- package/.claude/helpers/gate.cjs +170 -13
- package/.claude/skills/flfl/SKILL.md +50 -0
- package/bin/gate-hook.mjs +16 -0
- package/bin/gate.cjs +170 -13
- package/bin/lib/hook-io.mjs +19 -2
- package/bin/lib/internal-skills.mjs +5 -3
- package/bin/session-start-launcher.mjs +145 -12
- package/dist/src/cli/init/claudemd-generator.js +9 -4
- package/dist/src/cli/init/embedded-helpers.js +25 -0
- package/dist/src/cli/init/executor.js +3 -0
- package/dist/src/cli/init/helpers-generator.js +71 -1547
- package/dist/src/cli/version.js +1 -1
- package/package.json +4 -3
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Skills that ship in the npm tarball (under `node_modules/moflo/.claude/skills/`)
|
|
3
3
|
* but must NEVER be installed into consumer projects — strictly moflo-internal
|
|
4
4
|
* dev tooling. `/publish` bumps moflo's own version and publishes to npm;
|
|
5
|
-
* `/reset-epic` torches epic test data
|
|
6
|
-
* consumer
|
|
5
|
+
* `/reset-epic` torches epic test data; `/flfl` runs `/fl` preloaded with the
|
|
6
|
+
* rules that govern developing MOFLO (cross-platform, consumer blast radius,
|
|
7
|
+
* dogfooding), which are not constraints on a consumer's own project. All three
|
|
8
|
+
* are meaningless, noisy, or harmful in a consumer repo.
|
|
7
9
|
*
|
|
8
10
|
* The session-start launcher's recursive skills sync (`syncDirRecursive` in
|
|
9
11
|
* `file-sync.mjs`) copies every shipped skill into the consumer on each run, so
|
|
@@ -13,4 +15,4 @@
|
|
|
13
15
|
* so this leaf mirrors it. `tests/bin/internal-skills-parity.test.ts` asserts
|
|
14
16
|
* the two lists never drift.
|
|
15
17
|
*/
|
|
16
|
-
export const INTERNAL_SKILLS = ['publish', 'reset-epic'];
|
|
18
|
+
export const INTERNAL_SKILLS = ['publish', 'reset-epic', 'flfl'];
|
|
@@ -19,6 +19,7 @@ import { makeSyncer, contentEqual, syncDirRecursive } from './lib/file-sync.mjs'
|
|
|
19
19
|
import { INTERNAL_SKILLS } from './lib/internal-skills.mjs';
|
|
20
20
|
import { parseSkillCategories, computeExcludedSkills } from './lib/skill-categories.mjs';
|
|
21
21
|
import { loadShippedScripts } from './lib/shipped-scripts.mjs';
|
|
22
|
+
import { readHookStdin } from './lib/hook-io.mjs';
|
|
22
23
|
import {
|
|
23
24
|
readContinuityConfig,
|
|
24
25
|
readGitState,
|
|
@@ -651,7 +652,7 @@ function stopDaemon(lockFile) {
|
|
|
651
652
|
// error 'process can only be terminated forcefully'. The prior
|
|
652
653
|
// implementation invoked it anyway, swallowed the error, then polled
|
|
653
654
|
// alive for 3s before escalating — exactly the time-waste that pushed
|
|
654
|
-
// §3's stopDaemon past the
|
|
655
|
+
// §3's stopDaemon past the SessionStart hook timeout. Go
|
|
655
656
|
// straight to /F /T (tree-kill, in case a worker child outlived its
|
|
656
657
|
// parent) on Win.
|
|
657
658
|
if (process.platform === 'win32') {
|
|
@@ -753,23 +754,155 @@ function resolveDaemonRecyclerPath() {
|
|
|
753
754
|
}
|
|
754
755
|
|
|
755
756
|
// ── 2. Reset workflow state for new session ──────────────────────────────────
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
757
|
+
//
|
|
758
|
+
// #1441 — reset on a NEW session only. Claude Code fires SessionStart with a
|
|
759
|
+
// `source` of `startup`, `resume`, `clear` or `compact`, and moflo's settings
|
|
760
|
+
// entry carries no matcher, so this launcher runs for all four. A compaction
|
|
761
|
+
// is the SAME session continuing with a shorter context; a resume is the same
|
|
762
|
+
// session reopened. Resetting there wiped gate state mid-run, two ways:
|
|
763
|
+
//
|
|
764
|
+
// 1. `flMode` and `sddMode` are derived ONLY from the user's prompt text
|
|
765
|
+
// (gate.cjs applyPromptStateReset). Clearing them turned the #952
|
|
766
|
+
// swarm/hive invocation gate and the #1297 SDD gate OFF for the rest of
|
|
767
|
+
// the run — the user's next prompt is ordinary prose, so nothing ever
|
|
768
|
+
// re-armed them. A `/fl -s` run that compacted stopped enforcing
|
|
769
|
+
// swarm_init before Agent spawns, silently.
|
|
770
|
+
// 2. testsRun / simplifyRun / verifyRun / learningsStored and their
|
|
771
|
+
// fingerprints were discarded, so a compaction forced a full re-run of
|
|
772
|
+
// tests, /flo-simplify and /verify before `gh pr create`.
|
|
773
|
+
//
|
|
774
|
+
// Unknown, absent, or unparseable source resets — byte-identical to the old
|
|
775
|
+
// behaviour on a host that doesn't send the field, and the safe direction
|
|
776
|
+
// (gates armed rather than silently off).
|
|
777
|
+
const CONTINUING_SESSION_SOURCES = new Set(['compact', 'resume']);
|
|
778
|
+
const KNOWN_SESSION_SOURCES = new Set(['startup', 'clear', 'compact', 'resume']);
|
|
779
|
+
|
|
780
|
+
// …but a compaction is NOT a pure "keep everything" either. The credits above
|
|
781
|
+
// describe work that still stands — tests ran, /verify passed, the diff is
|
|
782
|
+
// unchanged. The memory-search credit is the one thing compaction genuinely
|
|
783
|
+
// invalidates: `memorySearched` says "this actor has the search results in
|
|
784
|
+
// context", and after a compaction it does not. Preserving it would hand the
|
|
785
|
+
// post-compaction model a satisfied memory gate over a context that no longer
|
|
786
|
+
// holds a single result — the exact "moflo isn't being used" symptom, arrived
|
|
787
|
+
// at from the opposite direction.
|
|
788
|
+
//
|
|
789
|
+
// `memoryRequired` is re-armed for the same reason, and it also closes a second
|
|
790
|
+
// hole reported against #1441: gate.cjs derives `memoryRequired` from the user's
|
|
791
|
+
// prompt TEXT, and `/compact` is an 8-character non-task string, so submitting it
|
|
792
|
+
// set `memoryRequired: false` and the scan/read gates went quiet until some later
|
|
793
|
+
// prompt happened to qualify. SessionStart fires AFTER that UserPromptSubmit, so
|
|
794
|
+
// re-arming here corrects it — and unlike a prompt-text rule it also covers AUTO
|
|
795
|
+
// compaction, where no `/compact` is ever typed.
|
|
796
|
+
//
|
|
797
|
+
// Resume is untouched by this: it reloads the conversation, so a prior search is
|
|
798
|
+
// still in context.
|
|
799
|
+
//
|
|
800
|
+
// Forcing `memoryRequired` on is deliberate even when the pre-compaction prompt
|
|
801
|
+
// was genuinely trivial and scored false on its own merits. One memory search is
|
|
802
|
+
// cheap; a context-blind model exploring files with the gate disarmed is the
|
|
803
|
+
// thing this whole issue is about.
|
|
804
|
+
const MEMORY_CREDIT_KEYS = ['memorySearched', 'memorySearchedBy', 'memoryRequired'];
|
|
805
|
+
|
|
806
|
+
// Full shape, not the 4-field literal this used to write. gate.cjs readState()
|
|
807
|
+
// merges STATE_DEFAULTS over whatever it parses, so the short shape behaved
|
|
808
|
+
// identically THERE — but it left a half-populated file for every other reader
|
|
809
|
+
// of workflow-state.json, and it silently drifted from STATE_DEFAULTS each time
|
|
810
|
+
// a gate field was added. tests/bin/launcher-1441-compact-preserves-state.test.ts
|
|
811
|
+
// pins these keys to gate.cjs's STATE_DEFAULTS so they cannot drift apart again.
|
|
812
|
+
function freshWorkflowState() {
|
|
813
|
+
return {
|
|
761
814
|
tasksCreated: false,
|
|
762
815
|
taskCount: 0,
|
|
816
|
+
tasksAcknowledged: false,
|
|
763
817
|
memorySearched: false,
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
818
|
+
memorySearchedBy: {},
|
|
819
|
+
memoryRequired: true,
|
|
820
|
+
learningsStored: false,
|
|
821
|
+
testsRun: false,
|
|
822
|
+
testsFingerprint: null,
|
|
823
|
+
simplifyRun: false,
|
|
824
|
+
simplifySnapshotSha: null,
|
|
825
|
+
simplifyFingerprint: null,
|
|
826
|
+
verifyRun: false,
|
|
827
|
+
verifyOutcome: null,
|
|
828
|
+
verifyFingerprint: null,
|
|
829
|
+
interactionCount: 0,
|
|
830
|
+
sessionStart: new Date().toISOString(),
|
|
831
|
+
lastBlockedAt: null,
|
|
832
|
+
lastNamespaceHint: '',
|
|
833
|
+
lastNamespaceHintEmittedBy: {},
|
|
834
|
+
flMode: null,
|
|
835
|
+
swarmInitialized: false,
|
|
836
|
+
hiveInitialized: false,
|
|
837
|
+
sddMode: false,
|
|
838
|
+
activeSddSlug: null,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Derived from freshWorkflowState(), not a second literal: a re-armed memory
|
|
843
|
+
// gate is by definition the fresh-session value of those keys, and one place to
|
|
844
|
+
// change beats two that agree only by inspection.
|
|
845
|
+
function rearmedMemoryState() {
|
|
846
|
+
const fresh = freshWorkflowState();
|
|
847
|
+
return Object.fromEntries(MEMORY_CREDIT_KEYS.map((key) => [key, fresh[key]]));
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Bounded at 500ms by readHookStdin and short-circuited on a TTY, so a withheld
|
|
851
|
+
// stdin cannot eat the 5000ms SessionStart budget (hook-block-hash.ts). Read
|
|
852
|
+
// here rather than at the top of the file so the cost lands next to its only
|
|
853
|
+
// consumer.
|
|
854
|
+
const hookPayload = await readHookStdin();
|
|
855
|
+
const sessionSource = typeof hookPayload?.source === 'string' ? hookPayload.source : '';
|
|
856
|
+
// A source we don't recognise still resets — but say so. Falling through in
|
|
857
|
+
// silence is how a renamed or newly-added "continuing" source would re-open
|
|
858
|
+
// this bug with nothing to notice it by; the reset is only the safe default
|
|
859
|
+
// while the set above is accurate.
|
|
860
|
+
if (sessionSource && !KNOWN_SESSION_SOURCES.has(sessionSource)) {
|
|
861
|
+
emitWarning(
|
|
862
|
+
`unrecognized SessionStart source "${sessionSource}" — resetting workflow state. ` +
|
|
863
|
+
'If this source continues an existing session, it belongs in §2\'s skip list (#1441).',
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
const stateDir = resolve(projectRoot, '.claude');
|
|
867
|
+
const stateFile = resolve(stateDir, 'workflow-state.json');
|
|
868
|
+
if (!CONTINUING_SESSION_SOURCES.has(sessionSource)) {
|
|
869
|
+
try {
|
|
870
|
+
if (!existsSync(stateDir)) mkdirSync(stateDir, { recursive: true });
|
|
871
|
+
writeFileSync(stateFile, JSON.stringify(freshWorkflowState(), null, 2));
|
|
872
|
+
} catch {
|
|
873
|
+
// Non-fatal - workflow gate will use defaults
|
|
874
|
+
}
|
|
875
|
+
} else if (sessionSource === 'compact') {
|
|
876
|
+
// Merge, never rewrite: everything the run has earned stays, only the memory
|
|
877
|
+
// credit is re-armed. Skipped entirely when there is no state file yet — a
|
|
878
|
+
// compaction before any prompt has nothing to re-arm, and writing a partial
|
|
879
|
+
// file here would defeat freshWorkflowState()'s shape guarantee.
|
|
880
|
+
//
|
|
881
|
+
// Read-modify-write, unsynchronised, like gate.cjs's own writeState. Safe
|
|
882
|
+
// here because a compaction quiesces the session: no tool hook is mid-flight
|
|
883
|
+
// to race with, and the next UserPromptSubmit is strictly after this hook.
|
|
884
|
+
// Leaving an unparseable file alone is also correct rather than merely
|
|
885
|
+
// tolerable — gate.cjs readState() falls back to STATE_DEFAULTS on a parse
|
|
886
|
+
// failure, which arms the memory gate. Repairing it here would only convert a
|
|
887
|
+
// fail-safe into an equivalent write.
|
|
888
|
+
try {
|
|
889
|
+
if (existsSync(stateFile)) {
|
|
890
|
+
const parsed = JSON.parse(readFileSync(stateFile, 'utf-8'));
|
|
891
|
+
writeFileSync(
|
|
892
|
+
stateFile,
|
|
893
|
+
JSON.stringify({ ...parsed, ...rearmedMemoryState() }, null, 2),
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
} catch (err) {
|
|
897
|
+
// Non-fatal, but not silent (#854): a failure here leaves the memory gate
|
|
898
|
+
// credited over a context that no longer holds the results.
|
|
899
|
+
emitWarning(`could not re-arm the memory gate after compaction (${errMessage(err)})`);
|
|
900
|
+
}
|
|
768
901
|
}
|
|
769
902
|
|
|
770
903
|
// ── 2a. Recycle daemon when behind installed version (#1054 follow-up) ──────
|
|
771
904
|
// Promoted from §3a-pre to run BEFORE §3's file-sync work. The launcher has
|
|
772
|
-
// a
|
|
905
|
+
// a 5000ms SessionStart hook timeout (src/cli/services/hook-block-hash.ts);
|
|
773
906
|
// §0c (DB repair) + §3 (file-sync, manifest, cherry-pick) + stopDaemon's
|
|
774
907
|
// up-to-4s graceful poll routinely exceeds it on upgrade sessions, killing
|
|
775
908
|
// the launcher mid-§3. Result: §3a-pre never ran on the very sessions that
|
|
@@ -1470,7 +1603,7 @@ try {
|
|
|
1470
1603
|
|
|
1471
1604
|
// ── 3a-pre. (removed) Daemon-version-skew recycle moved to §2a. ─────────────
|
|
1472
1605
|
// The previous version of this block ran AFTER §3's heavy file-sync work,
|
|
1473
|
-
// which routinely exceeded the 3000ms SessionStart hook timeout and was
|
|
1606
|
+
// which routinely exceeded the then-3000ms SessionStart hook timeout and was
|
|
1474
1607
|
// killed before reaching this point. §2a now runs early and force-kills the
|
|
1475
1608
|
// stale daemon before §3 can starve out. Don't restore §3a-pre — keep the
|
|
1476
1609
|
// recycle in one place so the two paths can't drift.
|
|
@@ -21,15 +21,19 @@ const LEGACY_MARKER_ENDS = [
|
|
|
21
21
|
];
|
|
22
22
|
/**
|
|
23
23
|
* The single moflo section injected into CLAUDE.md.
|
|
24
|
-
*
|
|
24
|
+
*
|
|
25
|
+
* This lands in EVERY consumer's CLAUDE.md and is read on every prompt, so it
|
|
26
|
+
* is a running token cost, not a doc. Keep it terse: state the rule, then point
|
|
27
|
+
* at moflo-core-guidance.md for the detail. Prefer cutting words over adding a
|
|
28
|
+
* line — anything needing a paragraph belongs in guidance, not here.
|
|
25
29
|
*/
|
|
26
30
|
function mofloSection() {
|
|
27
31
|
return `${MARKER_START}
|
|
28
32
|
## MoFlo — AI Agent Orchestration
|
|
29
33
|
|
|
30
|
-
### FIRST ACTION ON EVERY PROMPT: Search Memory
|
|
34
|
+
### FIRST ACTION ON EVERY PROMPT — AND EVERY TOPIC CHANGE: Search Memory
|
|
31
35
|
|
|
32
|
-
Your first tool call MUST be \`mcp__moflo__memory_search\` — before any Glob/Grep/Read. Pick the namespace by question shape: \`code-map\` for "where is symbol X defined", \`tests\` for "what tests cover Y", \`patterns\` for "what's our pattern for Z", \`guidance\` for project rules, \`learnings\` for "did we hit this before". Pivot on the bare symbol/keyword (not a natural-language question), and trust similarity ≥ 0.80 as a confident hit. When the user says "remember this", call \`mcp__moflo__memory_store\` with namespace \`learnings\`.
|
|
36
|
+
Your first tool call MUST be \`mcp__moflo__memory_search\` — before any Glob/Grep/Read or read-like Bash (\`cat\`, \`grep\`, \`node -e\`). **Search again on every new subject, mid-prompt** — new symbol, area, subsystem, or sub-question. A long task is many searches, not one. Pick the namespace by question shape: \`code-map\` for "where is symbol X defined", \`tests\` for "what tests cover Y", \`patterns\` for "what's our pattern for Z", \`guidance\` for project rules, \`learnings\` for "did we hit this before". Pivot on the bare symbol/keyword (not a natural-language question), and trust similarity ≥ 0.80 as a confident hit. When the user says "remember this", call \`mcp__moflo__memory_store\` with namespace \`learnings\`.
|
|
33
37
|
|
|
34
38
|
### Traverse chunks, don't bulk-retrieve
|
|
35
39
|
|
|
@@ -37,8 +41,9 @@ Search results carry a compact \`navigation\` crumb (parentDoc, prev/next, chunk
|
|
|
37
41
|
|
|
38
42
|
### Gates
|
|
39
43
|
|
|
40
|
-
- **Blocking** (hook exits non-zero): memory search before Glob/Grep/guidance Read; tests + \`/flo-simplify\` + learnings + \`/verify\` before \`gh pr create\`; \`swarm_init\`/\`hive-mind_init\` before Agent under \`/fl -s|-h\`.
|
|
44
|
+
- **Blocking** (hook exits non-zero): memory search before Glob/Grep/guidance Read and before read-like Bash/PowerShell commands; tests + \`/flo-simplify\` + learnings + \`/verify\` before \`gh pr create\`; \`swarm_init\`/\`hive-mind_init\` before Agent under \`/fl -s|-h\`.
|
|
41
45
|
- **Advisory** (reminder only): \`TaskCreate\` before spawning the Agent tool, entries in ICON+[Role] format — see \`.claude/guidance/moflo-task-icons.md\`.
|
|
46
|
+
- **Not enforced**: re-searching after a mid-prompt topic change — no hook sees the pivot. Do it anyway.
|
|
42
47
|
|
|
43
48
|
### Tools
|
|
44
49
|
|