moflo 4.12.4 → 4.12.5
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/guidance/shipped/moflo-agent-rules.md +1 -0
- package/.claude/guidance/shipped/moflo-claude-swarm-cohesion.md +1 -1
- package/.claude/guidance/shipped/moflo-cli-reference.md +4 -4
- package/.claude/guidance/shipped/moflo-core-guidance.md +4 -4
- package/.claude/guidance/shipped/moflo-guidance-rules.md +21 -0
- package/.claude/guidance/shipped/moflo-inline-fixes.md +98 -0
- package/.claude/guidance/shipped/moflo-root-cause-discipline.md +10 -10
- package/.claude/guidance/shipped/moflo-sdd.md +5 -5
- package/.claude/guidance/shipped/moflo-spell-engine.md +1 -1
- package/.claude/guidance/shipped/moflo-yaml-reference.md +8 -8
- package/.claude/helpers/gate.cjs +46 -7
- package/.claude/helpers/pr-create-command.cjs +440 -0
- package/.claude/skills/eldar/SKILL.md +1 -1
- package/.claude/skills/fl/SKILL.md +6 -6
- package/.claude/skills/fl/execution-modes.md +3 -3
- package/.claude/skills/fl/phases.md +8 -8
- package/.claude/skills/fl/sdd.md +6 -6
- package/.claude/skills/guidance/SKILL.md +3 -0
- package/.claude/skills/verify/SKILL.md +4 -4
- package/bin/gate.cjs +46 -7
- package/bin/hooks.mjs +4 -1
- package/bin/lib/retired-files.mjs +38 -6
- package/bin/lib/shipped-scripts.json +2 -1
- package/bin/lib/skill-categories.mjs +81 -17
- package/bin/pr-create-command.cjs +440 -0
- package/bin/session-start-launcher.mjs +11 -6
- package/dist/src/cli/aidefence/domain/entities/threat.js +2 -1
- package/dist/src/cli/commands/doctor-checks-deep.js +3 -2
- package/dist/src/cli/commands/doctor-checks-memory-access.js +3 -2
- package/dist/src/cli/commands/doctor-checks-swarm.js +3 -2
- package/dist/src/cli/commands/hooks.js +2 -1
- package/dist/src/cli/commands/mcp.js +3 -2
- package/dist/src/cli/commands/memory.js +2 -1
- package/dist/src/cli/commands/performance.js +2 -1
- package/dist/src/cli/commands/retire.js +1 -1
- package/dist/src/cli/commands/spell-schedule.js +2 -1
- package/dist/src/cli/commands/swarm.js +216 -104
- package/dist/src/cli/guidance/headless.js +5 -8
- package/dist/src/cli/guidance/retriever.js +27 -6
- package/dist/src/cli/guidance/ruvbot-integration.js +2 -1
- package/dist/src/cli/guidance/uncertainty.js +2 -1
- package/dist/src/cli/hooks/bridge/official-hooks-bridge.js +2 -1
- package/dist/src/cli/hooks/mcp/index.js +3 -2
- package/dist/src/cli/hooks/reasoningbank/index.js +2 -1
- package/dist/src/cli/hooks/registry/index.js +2 -1
- package/dist/src/cli/hooks/swarm/index.js +6 -5
- package/dist/src/cli/init/claudemd-generator.js +2 -1
- package/dist/src/cli/init/helpers-generator.js +1 -1
- package/dist/src/cli/init/moflo-yaml-template.js +4 -4
- package/dist/src/cli/mcp-client.js +5 -2
- package/dist/src/cli/mcp-tools/agent-tools.js +4 -1
- package/dist/src/cli/mcp-tools/hive-mind-tools.js +4 -3
- package/dist/src/cli/mcp-tools/hooks-tools.js +6 -5
- package/dist/src/cli/mcp-tools/json-store.js +3 -2
- package/dist/src/cli/mcp-tools/neural-tools.js +2 -1
- package/dist/src/cli/mcp-tools/performance-tools.js +2 -1
- package/dist/src/cli/mcp-tools/session-tools.js +4 -2
- package/dist/src/cli/mcp-tools/spell-tools.js +2 -1
- package/dist/src/cli/memory/bridge-core.js +8 -2
- package/dist/src/cli/memory/controllers/_shared.js +8 -2
- package/dist/src/cli/memory/controllers/nightly-learner.js +2 -1
- package/dist/src/cli/memory/domain/services/memory-domain-service.js +2 -1
- package/dist/src/cli/memory/entries-write.js +2 -1
- package/dist/src/cli/memory/intelligence.js +2 -1
- package/dist/src/cli/memory/persistent-sona.js +1 -3
- package/dist/src/cli/memory/types.js +2 -3
- package/dist/src/cli/memory/verify.js +3 -2
- package/dist/src/cli/movector/graph-analyzer.js +14 -3
- package/dist/src/cli/neural/pattern-learner.js +4 -2
- package/dist/src/cli/neural/reasoning-bank.js +2 -1
- package/dist/src/cli/neural/reasoningbank-adapter.js +2 -1
- package/dist/src/cli/neural/sona-manager.js +4 -3
- package/dist/src/cli/production/monitoring.js +2 -1
- package/dist/src/cli/services/daemon-service.js +57 -46
- package/dist/src/cli/services/daemon-spell-executor.js +3 -2
- package/dist/src/cli/services/headless-worker-executor.js +4 -2
- package/dist/src/cli/services/learning-service.js +2 -1
- package/dist/src/cli/services/worker-daemon.js +2 -1
- package/dist/src/cli/shared/hooks/example-usage.js +3 -2
- package/dist/src/cli/shared/hooks/safety/git-commit.js +11 -2
- package/dist/src/cli/shared/hooks/session-hooks.js +3 -2
- package/dist/src/cli/shared/hooks/task-hooks.js +2 -1
- package/dist/src/cli/shared/mcp/session-manager.js +2 -1
- package/dist/src/cli/shared/plugins/official/hive-mind-plugin.js +2 -1
- package/dist/src/cli/shared/plugins/official/maestro-plugin.js +3 -2
- package/dist/src/cli/shared/security/index.js +6 -2
- package/dist/src/cli/shared/utils/atomic-file-write.js +9 -1
- package/dist/src/cli/shared/utils/id.js +54 -0
- package/dist/src/cli/shared/utils/platform.js +36 -5
- package/dist/src/cli/spells/commands/destructive-pattern-checker.js +6 -1
- package/dist/src/cli/spells/core/dry-run-validator.js +2 -1
- package/dist/src/cli/spells/core/interpolation.js +8 -3
- package/dist/src/cli/spells/core/permission-resolver.js +2 -2
- package/dist/src/cli/spells/core/runner.js +2 -1
- package/dist/src/cli/spells/core/shell.js +7 -8
- package/dist/src/cli/spells/factory/runner-bridge.js +3 -2
- package/dist/src/cli/spells/factory/runner-factory.js +3 -2
- package/dist/src/cli/spells/scheduler/scheduler.js +2 -1
- package/dist/src/cli/swarm/consensus/gossip.js +2 -1
- package/dist/src/cli/swarm/federation-hub.js +5 -4
- package/dist/src/cli/swarm/queen-coordinator.js +5 -4
- package/dist/src/cli/swarm/shared/events.js +2 -1
- package/dist/src/cli/swarm/unified-coordinator.js +3 -2
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
- package/retired-files.json +0 -72
- package/dist/src/cli/mcp-tools/coordination-tools.js +0 -86
- package/dist/src/cli/shared/security/secure-random.js +0 -142
|
@@ -81,6 +81,7 @@ Apply the universal rules from `.claude/guidance/moflo-guidance-rules.md`. The r
|
|
|
81
81
|
7. Avoid the listed anti-patterns
|
|
82
82
|
8. Optimize for RAG chunking
|
|
83
83
|
9. End with a `## See Also` section
|
|
84
|
+
10. Never cite issue or PR numbers — write the rule, not where it came from
|
|
84
85
|
|
|
85
86
|
### Audience: Humans (`-h` flag) — lighter human ruleset
|
|
86
87
|
|
|
@@ -97,6 +98,7 @@ Drop the rules that serve Claude's RAG retrieval and imperative-mood enforcement
|
|
|
97
98
|
| 7 | Avoid anti-patterns | **Modified** | Short prose preambles ARE allowed for humans; code-comments-as-rules still bad |
|
|
98
99
|
| 8 | Optimize for RAG chunking | **Drop** | Humans don't query a vector index |
|
|
99
100
|
| 9 | `## See Also` section at end | **Keep** | Helps human readers traverse to related docs |
|
|
101
|
+
| 10 | No issue / PR citations | **Keep** | A bare issue number is unresolvable for any reader; provenance belongs in `git log` |
|
|
100
102
|
|
|
101
103
|
### Creating a new doc — scaffold this shape
|
|
102
104
|
|
|
@@ -139,6 +141,7 @@ For the loaded file, evaluate against the universal rules and report findings as
|
|
|
139
141
|
| H2 headings are specific (not "Overview", "Configuration", "Examples") | list any generic ones |
|
|
140
142
|
| Uses imperative voice for rules ("must"/"always"/"never") not hedged ("should"/"might"/"consider") | list hedged phrases found |
|
|
141
143
|
| Has prose preamble before first rule | yes / no |
|
|
144
|
+
| Cites issue or PR numbers (`(#NNNN)`, `issue #NNNN`, `Epic #NNNN`) | list every citation found |
|
|
142
145
|
|
|
143
146
|
Then propose edits as concrete diffs — never rewrite the whole file unless the user asks.
|
|
144
147
|
|
|
@@ -14,7 +14,7 @@ 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. **That alone does not open the gate
|
|
17
|
+
Invoking this skill (name `verify`) trips the `record-verify-run` hook, which flips the `verifyRun` state. **That alone does not open the gate**: `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
18
|
|
|
19
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.
|
|
20
20
|
|
|
@@ -26,7 +26,7 @@ Search memory before reading files, same as any task (satisfies `memory_first`,
|
|
|
26
26
|
mcp__moflo__memory_search { query: "verify <feature keywords>", namespace: "verify" }
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
**Search `verify`, never `learnings`.** Verdict records moved out of `learnings`
|
|
29
|
+
**Search `verify`, never `learnings`.** Verdict records moved out of `learnings` — see Step 5.
|
|
30
30
|
|
|
31
31
|
## Step 1 — Locate the acceptance criteria
|
|
32
32
|
|
|
@@ -97,13 +97,13 @@ mcp__moflo__memory_store {
|
|
|
97
97
|
}
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
**Store to `verify`, never `learnings
|
|
100
|
+
**Store to `verify`, never `learnings`.** A verdict is audit exhaust — one commit, one issue, criteria that never apply again — and `memory_search` returns a bounded set, so every verdict parked in `learnings` displaced a reusable lesson. The move is free: `record-learnings-stored` matches any `memory_store`, and `record-verify-outcome` keys on the `verify:` **key prefix**, not the namespace. Both gates still fire.
|
|
101
101
|
|
|
102
102
|
**Why both.** `value` is what gets embedded for semantic search, so it stays prose — a JSON blob there would degrade every future `verify` 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.
|
|
103
103
|
|
|
104
104
|
**`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.
|
|
105
105
|
|
|
106
|
-
**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
|
|
106
|
+
**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. `evidence` is therefore descriptive by necessity; a field named `exitCode` would be agent-narrated fiction, which is the problem this record exists to remove.
|
|
107
107
|
|
|
108
108
|
`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.
|
|
109
109
|
|
package/bin/gate.cjs
CHANGED
|
@@ -963,6 +963,45 @@ function creditIsLive(flag, stored, scope) {
|
|
|
963
963
|
return stored === now;
|
|
964
964
|
}
|
|
965
965
|
|
|
966
|
+
// #1410 — is this Bash command actually a `gh pr create` invocation? Delegates
|
|
967
|
+
// to pr-create-command.cjs, which sanitises data regions (quotes, heredoc
|
|
968
|
+
// bodies, comments) before looking for the command, so the gate neither misses
|
|
969
|
+
// real invocations (newline-separated, piped, parenthesised) nor fires on
|
|
970
|
+
// commands that merely quote the literal (`git commit -m "...gh pr create..."`).
|
|
971
|
+
//
|
|
972
|
+
// Fail-safe, in BOTH directions, because this runs on every Bash call in every
|
|
973
|
+
// consumer. gate-hook.mjs maps a non-zero exit from this script to exit 2, so an
|
|
974
|
+
// uncaught throw here does not degrade one gate — it blocks every Bash call the
|
|
975
|
+
// consumer makes. So a load failure (partial `.claude/helpers` sync mid-upgrade,
|
|
976
|
+
// hand-pruned install) AND a throw from the matcher itself both fall back to the
|
|
977
|
+
// pre-#1410 regex: previous behaviour, not a wedged session.
|
|
978
|
+
//
|
|
979
|
+
// Not silent (#854, hook-authoring §4): the fallback path advises on stderr and
|
|
980
|
+
// continues. If this ever fires it means the matcher crashed, which is worth
|
|
981
|
+
// being loud about — and it cannot spam a healthy session, because a healthy
|
|
982
|
+
// session never reaches it.
|
|
983
|
+
var LEGACY_PR_CREATE_RE = /(?:^|&&\s*|\|\|\s*|;\s*)\s*(?:[A-Z_][A-Z0-9_]*=\S+\s+)*gh\s+pr\s+create\b/;
|
|
984
|
+
var prCreateMatcher = null;
|
|
985
|
+
function isPrCreateCommand(cmd) {
|
|
986
|
+
if (prCreateMatcher === null) {
|
|
987
|
+
try {
|
|
988
|
+
prCreateMatcher = require('./pr-create-command.cjs').isPrCreateCommand;
|
|
989
|
+
} catch (e) {
|
|
990
|
+
prCreateMatcher = false;
|
|
991
|
+
process.stderr.write('moflo: pr-create-command.cjs unavailable (' + (e && e.message) + ') — PR gates fall back to the legacy matcher. Run `npx flo doctor --fix`.\n');
|
|
992
|
+
}
|
|
993
|
+
if (typeof prCreateMatcher !== 'function') prCreateMatcher = false;
|
|
994
|
+
}
|
|
995
|
+
if (prCreateMatcher) {
|
|
996
|
+
try {
|
|
997
|
+
return prCreateMatcher(cmd);
|
|
998
|
+
} catch (e) {
|
|
999
|
+
process.stderr.write('moflo: pr-create matcher threw (' + (e && e.message) + ') — falling back to the legacy matcher. Please report with the command that triggered it.\n');
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return LEGACY_PR_CREATE_RE.test(cmd);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
966
1005
|
// Classifier-aware simplify gate skip. Returns a string reason if the gate
|
|
967
1006
|
// can be auto-passed, or null if /simplify must run. Uses simplify-classify.cjs
|
|
968
1007
|
// so the gate's "trivial" definition matches the skill's exactly.
|
|
@@ -1600,12 +1639,12 @@ switch (command) {
|
|
|
1600
1639
|
break;
|
|
1601
1640
|
}
|
|
1602
1641
|
case 'check-before-pr': {
|
|
1603
|
-
// Anchored to command-start
|
|
1604
|
-
//
|
|
1605
|
-
//
|
|
1606
|
-
//
|
|
1642
|
+
// Anchored to command-start so heredoc bodies and quoted strings that
|
|
1643
|
+
// contain the literal "gh pr create" don't trip the gate during regular
|
|
1644
|
+
// `git commit -m "...gh pr create..."` flows, while still catching the
|
|
1645
|
+
// chained, piped, parenthesised, and multi-line shapes (#1410).
|
|
1607
1646
|
var cmd = process.env.TOOL_INPUT_command || '';
|
|
1608
|
-
if (
|
|
1647
|
+
if (!isPrCreateCommand(cmd)) break;
|
|
1609
1648
|
// #1374 — close the loop the TaskCreate reminder opens. Advisory: stdout,
|
|
1610
1649
|
// no exit, and worded as a reminder, because a message may only claim to
|
|
1611
1650
|
// block when it blocks (#1326). An open task list is a reporting failure,
|
|
@@ -1680,7 +1719,7 @@ switch (command) {
|
|
|
1680
1719
|
}
|
|
1681
1720
|
}
|
|
1682
1721
|
var missing = [];
|
|
1683
|
-
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
|
|
1722
|
+
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)');
|
|
1684
1723
|
if (config.simplify_gate && !s.simplifyRun) missing.push('/flo-simplify (or /distill) has not run since the last code edit');
|
|
1685
1724
|
if (config.learnings_gate && !s.learningsStored) missing.push('learnings have not been stored (call mcp__moflo__memory_store)');
|
|
1686
1725
|
if (missing.length === 0) break;
|
|
@@ -1716,7 +1755,7 @@ switch (command) {
|
|
|
1716
1755
|
// command (docs-only diffs are exempt, so this never blocks a docs PR).
|
|
1717
1756
|
if (!config.verify_before_done) break;
|
|
1718
1757
|
var cmd = process.env.TOOL_INPUT_command || '';
|
|
1719
|
-
if (
|
|
1758
|
+
if (!isPrCreateCommand(cmd)) break;
|
|
1720
1759
|
// No-source-files exemption — a docs-only / path-inert diff needs no verify.
|
|
1721
1760
|
var changedD = getChangedFilesVsBase();
|
|
1722
1761
|
if (changedD && changedD.length > 0) {
|
package/bin/hooks.mjs
CHANGED
|
@@ -23,6 +23,9 @@ import { spawn } from 'child_process';
|
|
|
23
23
|
import { existsSync, appendFileSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'fs';
|
|
24
24
|
import { resolve, dirname } from 'path';
|
|
25
25
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
26
|
+
// A builtin, deliberately: this file is a shipped hook entry point, and pulling
|
|
27
|
+
// in a bin/lib/ module here would widen its resolve closure for one call site.
|
|
28
|
+
import { randomBytes } from 'node:crypto';
|
|
26
29
|
import { createProcessManager } from './lib/process-manager.mjs';
|
|
27
30
|
import { shouldDaemonAutoStart } from './lib/daemon-config.mjs';
|
|
28
31
|
import { resolveMofloBin } from './lib/resolve-bin.mjs';
|
|
@@ -240,7 +243,7 @@ async function main() {
|
|
|
240
243
|
case 'pre-task': {
|
|
241
244
|
const description = getArg('description') || process.env.TOOL_INPUT_prompt;
|
|
242
245
|
if (description) {
|
|
243
|
-
const taskId = `task-${Date.now()}`;
|
|
246
|
+
const taskId = `task-${Date.now()}-${randomBytes(6).toString('hex')}`;
|
|
244
247
|
await runClaudeFlow('hooks', ['pre-task', '--task-id', taskId, '--description', description]);
|
|
245
248
|
}
|
|
246
249
|
break;
|
|
@@ -123,6 +123,8 @@ export function loadRetiredManifest(manifestPath) {
|
|
|
123
123
|
/**
|
|
124
124
|
* Decide what to do with a consumer-side path against a retirement entry:
|
|
125
125
|
*
|
|
126
|
+
* - 'shipped' — the installed package STILL ships this path, so the
|
|
127
|
+
* manifest entry contradicts the package; skip it entirely
|
|
126
128
|
* - 'absent' — path doesn't exist on disk; nothing to do
|
|
127
129
|
* - 'prune' — file exists and content hash matches a known-shipped
|
|
128
130
|
* value; safe to delete (consumer didn't customize)
|
|
@@ -131,13 +133,34 @@ export function loadRetiredManifest(manifestPath) {
|
|
|
131
133
|
* - 'unknown' — file exists but its hash couldn't be read (transient
|
|
132
134
|
* error); leave alone, retry next session
|
|
133
135
|
*
|
|
136
|
+
* The `shipped` check exists because a path can be retired and later RESTORED
|
|
137
|
+
* under the same name (#1414). The stale entry then holds only pre-deletion
|
|
138
|
+
* hashes, so the consumer's copy — which moflo itself just synced from the
|
|
139
|
+
* package — matches nothing and is reported as a customized retired file every
|
|
140
|
+
* session. Asking the package what it ships is the only source that cannot
|
|
141
|
+
* drift from the manifest, so it wins.
|
|
142
|
+
*
|
|
143
|
+
* `packageRoot` is optional and defaults to no cross-check: callers that don't
|
|
144
|
+
* know where the package lives keep exactly the pre-#1414 semantics.
|
|
145
|
+
*
|
|
146
|
+
* Order matters. The consumer-path check runs FIRST because most entries are
|
|
147
|
+
* `absent` on a typical install (already pruned, or never had the file), and
|
|
148
|
+
* those need no package stat at all — this runs on every consumer's session
|
|
149
|
+
* start, where a doubled stat count is 10-40ms on Windows with a scanner or a
|
|
150
|
+
* network `node_modules`. Entries that are absent locally are also exactly the
|
|
151
|
+
* ones a contradiction cannot hurt: there is nothing to delete or report.
|
|
152
|
+
*
|
|
134
153
|
* @param {string} projectRoot
|
|
135
154
|
* @param {{path:string, knownContentHashes:string[]}} entry
|
|
136
|
-
* @
|
|
155
|
+
* @param {string|null} [packageRoot] - root of the installed moflo package
|
|
156
|
+
* @returns {{ action: 'shipped'|'absent'|'prune'|'preserve'|'unknown', actualHash: string|null }}
|
|
137
157
|
*/
|
|
138
|
-
export function classifyRetiredFile(projectRoot, entry) {
|
|
158
|
+
export function classifyRetiredFile(projectRoot, entry, packageRoot = null) {
|
|
139
159
|
const abs = resolve(projectRoot, entry.path);
|
|
140
160
|
if (!existsSync(abs)) return { action: 'absent', actualHash: null };
|
|
161
|
+
if (packageRoot && existsSync(resolve(packageRoot, entry.path))) {
|
|
162
|
+
return { action: 'shipped', actualHash: null };
|
|
163
|
+
}
|
|
141
164
|
const actualHash = fileSha256(abs);
|
|
142
165
|
if (!actualHash) return { action: 'unknown', actualHash: null };
|
|
143
166
|
if (entry.knownContentHashes.includes(actualHash)) {
|
|
@@ -167,15 +190,24 @@ export function classifyRetiredFile(projectRoot, entry) {
|
|
|
167
190
|
* `preservedDetails` carries the same set with the manifest's retirement
|
|
168
191
|
* provenance attached, for the machine-readable record (#1307 finding 3).
|
|
169
192
|
*
|
|
193
|
+
* `shipped` collects entries the installed package still ships (#1414). They
|
|
194
|
+
* are neither pruned nor preserved — the manifest is simply wrong about them,
|
|
195
|
+
* and reporting them as retained is what produced the false banner. Nothing
|
|
196
|
+
* reads it today; it is here so the return value is a complete accounting of
|
|
197
|
+
* every entry's disposition rather than silently dropping one class, and so
|
|
198
|
+
* tests can assert the skip happened instead of inferring it from an absence.
|
|
199
|
+
*
|
|
170
200
|
* @param {string} projectRoot
|
|
171
201
|
* @param {string} manifestPath
|
|
172
|
-
* @
|
|
202
|
+
* @param {string|null} [packageRoot] - root of the installed moflo package
|
|
203
|
+
* @returns {{ pruned: string[], preserved: string[], preservedDetails: Array<{path:string, retiredIn?:string, retiredBy?:string}>, shipped: string[], unknown: string[], failed: Array<{path:string, message:string}> }}
|
|
173
204
|
*/
|
|
174
|
-
export function applyRetiredPrune(projectRoot, manifestPath) {
|
|
205
|
+
export function applyRetiredPrune(projectRoot, manifestPath, packageRoot = null) {
|
|
175
206
|
const { entries } = loadRetiredManifest(manifestPath);
|
|
176
|
-
const report = { pruned: [], preserved: [], preservedDetails: [], unknown: [], failed: [] };
|
|
207
|
+
const report = { pruned: [], preserved: [], preservedDetails: [], shipped: [], unknown: [], failed: [] };
|
|
177
208
|
for (const entry of entries) {
|
|
178
|
-
const { action } = classifyRetiredFile(projectRoot, entry);
|
|
209
|
+
const { action } = classifyRetiredFile(projectRoot, entry, packageRoot);
|
|
210
|
+
if (action === 'shipped') { report.shipped.push(entry.path); continue; }
|
|
179
211
|
if (action === 'absent') continue;
|
|
180
212
|
if (action === 'preserve') {
|
|
181
213
|
report.preserved.push(entry.path);
|
|
@@ -74,6 +74,13 @@ export const SKILL_CATEGORY_NAMES = Object.keys(SKILL_CATEGORIES_MAP);
|
|
|
74
74
|
*/
|
|
75
75
|
export const ALWAYS_INSTALLED_SKILLS = ['flo', 'fl'];
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* How far `parseSkillCategories` will look ahead for the `]` closing a
|
|
79
|
+
* flow-style `categories: [...]` list. Bounded so an unterminated bracket costs
|
|
80
|
+
* a fixed amount of work rather than a scan to end-of-file per candidate line.
|
|
81
|
+
*/
|
|
82
|
+
const FLOW_LOOKAHEAD_LINES = 50;
|
|
83
|
+
|
|
77
84
|
/**
|
|
78
85
|
* Extract the selected skill categories from raw `moflo.yaml` text.
|
|
79
86
|
*
|
|
@@ -89,6 +96,14 @@ export const ALWAYS_INSTALLED_SKILLS = ['flo', 'fl'];
|
|
|
89
96
|
* - core
|
|
90
97
|
* - memory
|
|
91
98
|
*
|
|
99
|
+
* Scanned line by line rather than with one multi-line regex. The regex form
|
|
100
|
+
* spanned the gap between `skills:` and `categories:` with
|
|
101
|
+
* `(?:[ \t]+[^\r\n]*\r?\n)*?`, whose `[ \t]+` and `[^\r\n]*` overlap — every
|
|
102
|
+
* indented line could be split many ways, so a `skills:` block with no
|
|
103
|
+
* `categories:` key backtracked exponentially and hung the launcher at session
|
|
104
|
+
* start on the consumer's own `moflo.yaml` (#1418). Line scanning is linear and
|
|
105
|
+
* matches the same inputs.
|
|
106
|
+
*
|
|
92
107
|
* @param {string} yamlContent
|
|
93
108
|
* @returns {string[]|null} selected categories, or null when unconfigured
|
|
94
109
|
* (meaning "no restriction" — sync everything).
|
|
@@ -102,29 +117,78 @@ export const ALWAYS_INSTALLED_SKILLS = ['flo', 'fl'];
|
|
|
102
117
|
export function parseSkillCategories(yamlContent) {
|
|
103
118
|
if (typeof yamlContent !== 'string' || yamlContent.length === 0) return null;
|
|
104
119
|
|
|
105
|
-
|
|
106
|
-
const flow = yamlContent.match(/^[ \t]*skills:[ \t]*\r?\n(?:[ \t]+[^\r\n]*\r?\n)*?[ \t]+categories:[ \t]*\[([^\]]*)\]/m);
|
|
107
|
-
if (flow) {
|
|
108
|
-
return flow[1]
|
|
109
|
-
.split(',')
|
|
110
|
-
.map((s) => s.trim().replace(/^['"]|['"]$/g, ''))
|
|
111
|
-
.filter((s) => s.length > 0);
|
|
112
|
-
}
|
|
120
|
+
const lines = yamlContent.split(/\r?\n/);
|
|
113
121
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
for (let i = 0; i < lines.length; i++) {
|
|
123
|
+
if (!/^[ \t]*skills:[ \t]*$/.test(lines[i])) continue;
|
|
124
|
+
|
|
125
|
+
// Walk the indented block under `skills:`. A dedent (or a blank line) ends
|
|
126
|
+
// it, exactly as the old `[ \t]+`-per-line repetition required.
|
|
127
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
128
|
+
const line = lines[j];
|
|
129
|
+
if (!/^[ \t]/.test(line)) break;
|
|
130
|
+
|
|
131
|
+
// Flow style: categories: [a, b], possibly wrapped across lines — the old
|
|
132
|
+
// regex accepted that because its `[^\]]*` spanned newlines, so dropping
|
|
133
|
+
// it would be a silent behaviour regression.
|
|
134
|
+
//
|
|
135
|
+
// Lookahead is bounded and accumulates FORWARD rather than re-slicing the
|
|
136
|
+
// remainder of the file. `lines.slice(j).join('\n')` on every candidate
|
|
137
|
+
// line is O(n^2) on a file with many unterminated `categories: [` lines —
|
|
138
|
+
// a smaller version of exactly the attacker-controlled-YAML blowup this
|
|
139
|
+
// rewrite exists to close. A real flow list is one or two lines; 50 is
|
|
140
|
+
// already far past any legitimate input.
|
|
141
|
+
if (/^[ \t]+categories:[ \t]*\[/.test(line)) {
|
|
142
|
+
let buffer = line;
|
|
143
|
+
for (let k = j + 1; k < lines.length && k <= j + FLOW_LOOKAHEAD_LINES; k++) {
|
|
144
|
+
if (buffer.includes(']')) break;
|
|
145
|
+
buffer += '\n' + lines[k];
|
|
146
|
+
}
|
|
147
|
+
const flow = buffer.match(/^[ \t]+categories:[ \t]*\[([^\]]*)\]/);
|
|
148
|
+
if (flow) {
|
|
149
|
+
return flow[1]
|
|
150
|
+
.split(',')
|
|
151
|
+
.map((s) => s.trim().replace(/^['"]|['"]$/g, ''))
|
|
152
|
+
.filter((s) => s.length > 0);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Block style: categories:\n - a\n - b
|
|
157
|
+
if (/^[ \t]+categories:[ \t]*$/.test(line)) return parseBlockItems(lines, j + 1);
|
|
158
|
+
}
|
|
123
159
|
}
|
|
124
160
|
|
|
125
161
|
return null;
|
|
126
162
|
}
|
|
127
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Collect `- item` lines following a bare `categories:` key.
|
|
166
|
+
*
|
|
167
|
+
* Returns `null` when there is not a single dash item, mirroring the old block
|
|
168
|
+
* regex's `+` quantifier failing to match: a `categories:` key with nothing
|
|
169
|
+
* under it is unconfigured ("sync everything"), NOT an empty selection ("sync
|
|
170
|
+
* nothing"). Conflating those would strip every skill from a consumer who left
|
|
171
|
+
* the key dangling (Rule #2).
|
|
172
|
+
*
|
|
173
|
+
* @param {string[]} lines
|
|
174
|
+
* @param {number} start - index of the first candidate item line
|
|
175
|
+
* @returns {string[]|null}
|
|
176
|
+
*/
|
|
177
|
+
function parseBlockItems(lines, start) {
|
|
178
|
+
const items = [];
|
|
179
|
+
let sawItem = false;
|
|
180
|
+
|
|
181
|
+
for (let k = start; k < lines.length; k++) {
|
|
182
|
+
const item = lines[k].match(/^[ \t]*-[ \t]*(.+?)[ \t]*$/);
|
|
183
|
+
if (!item) break;
|
|
184
|
+
sawItem = true;
|
|
185
|
+
const value = item[1].replace(/^['"]|['"]$/g, '').trim();
|
|
186
|
+
if (value.length > 0) items.push(value);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return sawItem ? items : null;
|
|
190
|
+
}
|
|
191
|
+
|
|
128
192
|
/**
|
|
129
193
|
* Resolve the set of top-level skill directory names the launcher must NOT
|
|
130
194
|
* sync, given a selection.
|