@massu/core 2.5.5 → 2.5.7
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/CHANGELOG.md +63 -0
- package/dist/cli.js +167 -46
- package/dist/hooks/auto-learning-pipeline.js +81 -16
- package/dist/hooks/classify-failure.js +90 -652
- package/dist/hooks/cost-tracker.js +69 -4
- package/dist/hooks/fix-detector.js +79 -12
- package/dist/hooks/incident-pipeline.js +72 -7
- package/dist/hooks/intent-suggester.js +69 -4
- package/dist/hooks/memory-recall.js +89 -6
- package/dist/hooks/post-edit-context.js +69 -4
- package/dist/hooks/post-tool-use.js +76 -11
- package/dist/hooks/pre-compact.js +69 -4
- package/dist/hooks/pre-delete-check.js +446 -71
- package/dist/hooks/pre-tool-use-gate.js +237 -223
- package/dist/hooks/quality-event.js +69 -4
- package/dist/hooks/rule-enforcement-pipeline.js +72 -7
- package/dist/hooks/security-gate.js +611 -181
- package/dist/hooks/session-end.js +607 -73
- package/dist/hooks/session-start.js +112 -8
- package/dist/hooks/user-prompt.js +83 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,69 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
10
10
|
> redistributed under the **Apache License 2.0**. Full license + NOTICE:
|
|
11
11
|
> `packages/core/assets/embedder/MODEL-LICENSE`.
|
|
12
12
|
|
|
13
|
+
## [2.5.7] - 2026-11-10
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **Cloud sync no longer queues uploads the server refuses.** When the massu cloud refused an upload
|
|
18
|
+
for a reason that retrying cannot change (for example `403 Sync not available for current plan
|
|
19
|
+
status`), massu queued it and retried forever. One project's memory database grew to 8.5 GB this way,
|
|
20
|
+
7.6 GB of it refused uploads, and every reply re-sent a ~179 KB upload that was always refused. Now:
|
|
21
|
+
- a refusal that cannot succeed is never queued or retried;
|
|
22
|
+
- when the account itself is refused (401 or 403), massu records the server's reason, pauses sync,
|
|
23
|
+
clears the queue (learned rules are kept), and shows `MASSU CLOUD SYNC PAUSED` with that reason at
|
|
24
|
+
session start. It checks again once a day and resumes on its own when the account allows it;
|
|
25
|
+
- temporary failures (network, 5xx, 429) still queue, but the queue now holds at most 500 items and
|
|
26
|
+
nothing older than 30 days, and every item gets its turn.
|
|
27
|
+
- **`massu install-hooks` now upgrades a project completely.** It also moves the massu MCP server in
|
|
28
|
+
`.mcp.json` to the same version as the hooks (it never did before, so every upgrade left the server
|
|
29
|
+
behind), and it replaces hook entries written by much older installers, which used to stay beside the
|
|
30
|
+
current ones and fail on every tool call. Your own hooks and any hand-written massu entry are left alone.
|
|
31
|
+
- **A hook launched through a symlinked path now runs.** Started through a symlink, a hook decided it was
|
|
32
|
+
not the program being run and exited without doing anything; for the safety gate that meant allowing
|
|
33
|
+
the call. The commands `massu init` writes were not affected.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- **The first prompt of a session is faster.** Session start now loads the memory-search model in the
|
|
38
|
+
background, so the first prompt no longer pays for it (measured: about 1.5 s for the first search,
|
|
39
|
+
0.6 s after). Turn it off with `MASSU_RECALL_WARMUP=0`.
|
|
40
|
+
- **`scripts/massu-db-size-audit.sh`** (in the repository) reports where a massu database's space goes:
|
|
41
|
+
live data, space freed but not yet returned to the disk, the largest tables, and the sync queue.
|
|
42
|
+
|
|
43
|
+
## [2.5.6] - 2026-11-09
|
|
44
|
+
|
|
45
|
+
### Security
|
|
46
|
+
|
|
47
|
+
- **The safety gate blocks again.** In 2.5.2 through 2.5.5 the PreToolUse gate that `massu init`
|
|
48
|
+
installs warned about everything it should have blocked and then let the call run: commands
|
|
49
|
+
that read `.env` and other secret files, piping a download into a shell, `rm -rf /`, disk
|
|
50
|
+
formatting and writes to `/etc/passwd`. Claude Code blocks a tool call only when the hook exits
|
|
51
|
+
with code 2; the gate printed "BLOCKED" and exited 0. It now exits 2 for every blocking
|
|
52
|
+
finding. **Upgrade, then run `massu init` again** so your settings pick up the change below.
|
|
53
|
+
- **A gate that fails now blocks.** `massu init` sets Claude Code's `"onFailure": "block"`
|
|
54
|
+
(Claude Code 2.1.295 or later) on the gate, so a gate that crashes, times out or prints invalid
|
|
55
|
+
output blocks the call instead of letting it through. On older Claude Code, the `massu-hook`
|
|
56
|
+
launcher and `hook-runner` turn a crashed gate into a block.
|
|
57
|
+
- **The older hook names decide too.** Settings written before 1.12.0 may still name the
|
|
58
|
+
`security-gate` and `pre-delete-check` hooks. They used to warn only; they now make the same
|
|
59
|
+
decision as the gate.
|
|
60
|
+
- **More secret files are recognised:** secrets data files under any name, such as
|
|
61
|
+
`config.secrets.yaml` or `app.secret.json`. Source files such as `secrets.ts` are still allowed.
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- **The gate no longer refuses good calls under load.** A slow-arriving tool call could be
|
|
66
|
+
denied as "Unexpected end of JSON input" and pass on retry. The gate now reads its whole
|
|
67
|
+
input before deciding.
|
|
68
|
+
|
|
69
|
+
### Added
|
|
70
|
+
|
|
71
|
+
- **`massu_cost_feature` reports real data.** Each session's priced cost is split across the
|
|
72
|
+
registered features whose files it touched, in proportion to those touches; activity on files
|
|
73
|
+
that belong to no registered feature is shown as `(no registered feature)`, so the rows add up
|
|
74
|
+
to the sessions' total. Sessions whose cost is unknown are left out.
|
|
75
|
+
|
|
13
76
|
## [2.5.5] - 2026-11-09
|
|
14
77
|
|
|
15
78
|
### Fixed
|
package/dist/cli.js
CHANGED
|
@@ -2490,6 +2490,8 @@ __export(memory_db_exports, {
|
|
|
2490
2490
|
MEMORY_FILE_TITLE_LIKE: () => MEMORY_FILE_TITLE_LIKE,
|
|
2491
2491
|
MEMORY_FILE_TITLE_PREFIX: () => MEMORY_FILE_TITLE_PREFIX,
|
|
2492
2492
|
MEMORY_SCHEMA_VERSION: () => MEMORY_SCHEMA_VERSION,
|
|
2493
|
+
PENDING_SYNC_MAX_AGE_DAYS: () => PENDING_SYNC_MAX_AGE_DAYS,
|
|
2494
|
+
PENDING_SYNC_MAX_ITEMS: () => PENDING_SYNC_MAX_ITEMS,
|
|
2493
2495
|
TOOL_COST_EVENTS_RETENTION_DAYS: () => TOOL_COST_EVENTS_RETENTION_DAYS,
|
|
2494
2496
|
USAGE_COUNTER_ARMED_KEY: () => USAGE_COUNTER_ARMED_KEY,
|
|
2495
2497
|
addConversationTurn: () => addConversationTurn,
|
|
@@ -2498,6 +2500,7 @@ __export(memory_db_exports, {
|
|
|
2498
2500
|
addSummary: () => addSummary,
|
|
2499
2501
|
addToolCallDetail: () => addToolCallDetail,
|
|
2500
2502
|
addUserPrompt: () => addUserPrompt,
|
|
2503
|
+
allPendingSync: () => allPendingSync,
|
|
2501
2504
|
appendIncidentToFailureClass: () => appendIncidentToFailureClass,
|
|
2502
2505
|
armUsageCounter: () => armUsageCounter,
|
|
2503
2506
|
assignImportance: () => assignImportance,
|
|
@@ -2505,6 +2508,7 @@ __export(memory_db_exports, {
|
|
|
2505
2508
|
createSession: () => createSession,
|
|
2506
2509
|
deduplicateFailedAttempt: () => deduplicateFailedAttempt,
|
|
2507
2510
|
dequeuePendingSync: () => dequeuePendingSync,
|
|
2511
|
+
discardPendingSync: () => discardPendingSync,
|
|
2508
2512
|
embedMissingObservations: () => embedMissingObservations,
|
|
2509
2513
|
endSession: () => endSession,
|
|
2510
2514
|
enqueueRulePromotionEvent: () => enqueueRulePromotionEvent,
|
|
@@ -3487,6 +3491,35 @@ function initMemorySchema(db) {
|
|
|
3487
3491
|
}
|
|
3488
3492
|
function enqueueSyncPayload(db, payload) {
|
|
3489
3493
|
db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
|
|
3494
|
+
const overflow = db.prepare(
|
|
3495
|
+
`SELECT id, retry_count, last_error, payload FROM pending_sync
|
|
3496
|
+
WHERE created_at < datetime('now', ?)
|
|
3497
|
+
OR id NOT IN (SELECT id FROM pending_sync ORDER BY created_at DESC, id DESC LIMIT ?)
|
|
3498
|
+
LIMIT 10000`
|
|
3499
|
+
).all(`-${PENDING_SYNC_MAX_AGE_DAYS} days`, PENDING_SYNC_MAX_ITEMS);
|
|
3500
|
+
if (overflow.length > 0) {
|
|
3501
|
+
discardPendingSync(db, overflow, `over the queue limit (${PENDING_SYNC_MAX_ITEMS} items / ${PENDING_SYNC_MAX_AGE_DAYS} days)`);
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
function discardPendingSync(db, rows, why) {
|
|
3505
|
+
if (rows.length === 0) return 0;
|
|
3506
|
+
const rescued = rescueLearningFromStalePayloads(db, rows);
|
|
3507
|
+
for (let i = 0; i < rows.length; i += 500) {
|
|
3508
|
+
const ids = rows.slice(i, i + 500).map((r3) => r3.id);
|
|
3509
|
+
db.prepare(`DELETE FROM pending_sync WHERE id IN (${ids.map(() => "?").join(",")})`).run(...ids);
|
|
3510
|
+
}
|
|
3511
|
+
if (rescued > 0) {
|
|
3512
|
+
process.stderr.write(
|
|
3513
|
+
`[massu] RESCUED ${rescued} learned rule(s) from ${rows.length} discarded payload(s) \u2014 they were re-queued for delivery, NOT discarded.
|
|
3514
|
+
`
|
|
3515
|
+
);
|
|
3516
|
+
}
|
|
3517
|
+
process.stderr.write(`[massu] cloud sync: discarded ${rows.length} queued upload(s): ${why}.
|
|
3518
|
+
`);
|
|
3519
|
+
return rows.length;
|
|
3520
|
+
}
|
|
3521
|
+
function allPendingSync(db) {
|
|
3522
|
+
return db.prepare("SELECT id, retry_count, last_error, payload FROM pending_sync").all();
|
|
3490
3523
|
}
|
|
3491
3524
|
function getMemoryMeta(db, key) {
|
|
3492
3525
|
const row = db.prepare("SELECT value FROM memory_meta WHERE key = ?").get(key);
|
|
@@ -3689,7 +3722,9 @@ function dequeuePendingSync(db, limit = 10) {
|
|
|
3689
3722
|
}
|
|
3690
3723
|
}
|
|
3691
3724
|
return db.prepare(
|
|
3692
|
-
|
|
3725
|
+
// Least-retried first: oldest-first retried the same 10 items until they were discarded
|
|
3726
|
+
// while every newer item waited at retry 0 (2026-11-09: 43,492 never attempted).
|
|
3727
|
+
"SELECT id, payload, retry_count FROM pending_sync ORDER BY retry_count ASC, created_at ASC LIMIT ?"
|
|
3693
3728
|
).all(limit);
|
|
3694
3729
|
}
|
|
3695
3730
|
function removePendingSync(db, id) {
|
|
@@ -4362,7 +4397,7 @@ function scoreFailureClasses(db, matchText, filePath, promptContext, weights) {
|
|
|
4362
4397
|
}
|
|
4363
4398
|
return bestMatch;
|
|
4364
4399
|
}
|
|
4365
|
-
var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
|
|
4400
|
+
var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, PENDING_SYNC_MAX_ITEMS, PENDING_SYNC_MAX_AGE_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
|
|
4366
4401
|
var init_memory_db = __esm({
|
|
4367
4402
|
"src/memory-db.ts"() {
|
|
4368
4403
|
"use strict";
|
|
@@ -4377,6 +4412,8 @@ var init_memory_db = __esm({
|
|
|
4377
4412
|
init_memory_embed_sweep();
|
|
4378
4413
|
MEMORY_SCHEMA_VERSION = 2;
|
|
4379
4414
|
TOOL_COST_EVENTS_RETENTION_DAYS = 90;
|
|
4415
|
+
PENDING_SYNC_MAX_ITEMS = 500;
|
|
4416
|
+
PENDING_SYNC_MAX_AGE_DAYS = 30;
|
|
4380
4417
|
MAX_DRAFT_TEXT_LEN = 16384;
|
|
4381
4418
|
FUNNEL_EVENT_OUTBOX_CAP = 2e4;
|
|
4382
4419
|
_temporalColCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -5893,6 +5930,42 @@ var init_hook_timeouts = __esm({
|
|
|
5893
5930
|
}
|
|
5894
5931
|
});
|
|
5895
5932
|
|
|
5933
|
+
// src/lib/hook-registry.ts
|
|
5934
|
+
function getExpectedHookFiles() {
|
|
5935
|
+
return REGISTERED_HOOKS.map((name) => `${name}.js`);
|
|
5936
|
+
}
|
|
5937
|
+
var REGISTERED_HOOKS, DECISION_HOOKS;
|
|
5938
|
+
var init_hook_registry = __esm({
|
|
5939
|
+
"src/lib/hook-registry.ts"() {
|
|
5940
|
+
"use strict";
|
|
5941
|
+
REGISTERED_HOOKS = [
|
|
5942
|
+
"auto-learning-pipeline",
|
|
5943
|
+
"classify-failure",
|
|
5944
|
+
"cost-tracker",
|
|
5945
|
+
"fix-detector",
|
|
5946
|
+
"incident-pipeline",
|
|
5947
|
+
"intent-suggester",
|
|
5948
|
+
"memory-recall",
|
|
5949
|
+
"post-edit-context",
|
|
5950
|
+
"post-tool-use",
|
|
5951
|
+
"pre-compact",
|
|
5952
|
+
// P-E-019 (1.12.0): security-gate + pre-delete-check are kept registered
|
|
5953
|
+
// for back-compat (operator settings.json may still reference them
|
|
5954
|
+
// individually), but new installs emit only `pre-tool-use-gate` —
|
|
5955
|
+
// the consolidated single-spawn PreToolUse hook.
|
|
5956
|
+
"pre-delete-check",
|
|
5957
|
+
"pre-tool-use-gate",
|
|
5958
|
+
"quality-event",
|
|
5959
|
+
"rule-enforcement-pipeline",
|
|
5960
|
+
"security-gate",
|
|
5961
|
+
"session-end",
|
|
5962
|
+
"session-start",
|
|
5963
|
+
"user-prompt"
|
|
5964
|
+
];
|
|
5965
|
+
DECISION_HOOKS = ["pre-delete-check", "pre-tool-use-gate", "security-gate"];
|
|
5966
|
+
}
|
|
5967
|
+
});
|
|
5968
|
+
|
|
5896
5969
|
// src/detect/manifest-registry.ts
|
|
5897
5970
|
var manifest_registry_exports = {};
|
|
5898
5971
|
__export(manifest_registry_exports, {
|
|
@@ -16410,6 +16483,7 @@ ${import_picocolors2.default.gray(m2)} ${s}
|
|
|
16410
16483
|
// src/commands/init.ts
|
|
16411
16484
|
var init_exports = {};
|
|
16412
16485
|
__export(init_exports, {
|
|
16486
|
+
SUPERSEDED_MASSU_HOOKS: () => SUPERSEDED_MASSU_HOOKS,
|
|
16413
16487
|
applyVariantTemplate: () => applyVariantTemplate,
|
|
16414
16488
|
buildConfigFromDetection: () => buildConfigFromDetection,
|
|
16415
16489
|
buildHooksConfig: () => buildHooksConfig,
|
|
@@ -16432,6 +16506,7 @@ __export(init_exports, {
|
|
|
16432
16506
|
mergeHooksConfig: () => mergeHooksConfig,
|
|
16433
16507
|
parseInitArgs: () => parseInitArgs,
|
|
16434
16508
|
printInitHelp: () => printInitHelp,
|
|
16509
|
+
refreshMcpServerPin: () => refreshMcpServerPin,
|
|
16435
16510
|
registerMcpServer: () => registerMcpServer,
|
|
16436
16511
|
renderConfigYaml: () => renderConfigYaml,
|
|
16437
16512
|
resolveHooksDir: () => resolveHooksDir,
|
|
@@ -17016,6 +17091,32 @@ function getInstallerVersion() {
|
|
|
17016
17091
|
"getInstallerVersion: could not resolve @massu/core package.json. This indicates a corrupt install. Re-install via `npx -y @massu/core init`."
|
|
17017
17092
|
);
|
|
17018
17093
|
}
|
|
17094
|
+
function refreshMcpServerPin(projectRoot) {
|
|
17095
|
+
const mcpPath = resolve12(projectRoot, ".mcp.json");
|
|
17096
|
+
if (!existsSync21(mcpPath)) return "absent";
|
|
17097
|
+
let doc;
|
|
17098
|
+
try {
|
|
17099
|
+
doc = JSON.parse(readFileSync17(mcpPath, "utf-8"));
|
|
17100
|
+
} catch {
|
|
17101
|
+
return "custom";
|
|
17102
|
+
}
|
|
17103
|
+
const servers = doc.mcpServers ?? {};
|
|
17104
|
+
const cur = servers.massu;
|
|
17105
|
+
if (!cur) return "absent";
|
|
17106
|
+
const args2 = Array.isArray(cur.args) ? cur.args.map(String) : [];
|
|
17107
|
+
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
17108
|
+
const shimForm = typeof cur.command === "string" && /[/\\]massu-hook$/.test(cur.command) && args2.length === 1 && SEMVER.test(args2[0]);
|
|
17109
|
+
const npxForm = cur.command === "npx" && args2.length === 2 && args2[0] === "-y" && /^@massu\/core@/.test(args2[1]);
|
|
17110
|
+
if (!shimForm && !npxForm) return "custom";
|
|
17111
|
+
const version = getInstallerVersion();
|
|
17112
|
+
const shim = resolveMassuShim();
|
|
17113
|
+
const next = shim !== null ? { type: "stdio", command: shim, args: [version] } : { type: "stdio", command: "npx", args: ["-y", `@massu/core@${version}`] };
|
|
17114
|
+
if (JSON.stringify({ ...cur, ...next }) === JSON.stringify(cur)) return "current";
|
|
17115
|
+
servers.massu = { ...cur, ...next };
|
|
17116
|
+
doc.mcpServers = servers;
|
|
17117
|
+
writeFileSync5(mcpPath, JSON.stringify(doc, null, 2) + "\n", "utf-8");
|
|
17118
|
+
return "updated";
|
|
17119
|
+
}
|
|
17019
17120
|
function registerMcpServer(projectRoot) {
|
|
17020
17121
|
const mcpPath = resolve12(projectRoot, ".mcp.json");
|
|
17021
17122
|
let existing = {};
|
|
@@ -17102,6 +17203,22 @@ fi
|
|
|
17102
17203
|
MASSU_VERSION="$1"
|
|
17103
17204
|
shift
|
|
17104
17205
|
MASSU_CLI="\${HOME}/.massu/runtime/\${MASSU_VERSION}/node_modules/@massu/core/dist/cli.js"
|
|
17206
|
+
# A DECIDING hook's exit code is its verdict: 0 allows, 2 denies. If the launch itself fails
|
|
17207
|
+
# (a runtime that cannot load exits 1), Claude Code would RUN the call, so for these hooks
|
|
17208
|
+
# any other status becomes a deny. Every other subcommand keeps the exec path below.
|
|
17209
|
+
if [ "\${1:-}" = hook-runner ]; then
|
|
17210
|
+
case "\${2:-}" in
|
|
17211
|
+
${DECISION_HOOKS.join("|")})
|
|
17212
|
+
if [ -f "$MASSU_CLI" ]; then node "$MASSU_CLI" "$@"; else npx -y "@massu/core@\${MASSU_VERSION}" "$@"; fi
|
|
17213
|
+
st=$?
|
|
17214
|
+
if [ "$st" -ne 0 ] && [ "$st" -ne 2 ]; then
|
|
17215
|
+
echo "MASSU SECURITY GATE \u2014 BLOCKED: the $2 hook ended without a verdict (exit $st)" >&2
|
|
17216
|
+
exit 2
|
|
17217
|
+
fi
|
|
17218
|
+
exit "$st"
|
|
17219
|
+
;;
|
|
17220
|
+
esac
|
|
17221
|
+
fi
|
|
17105
17222
|
if [ -f "$MASSU_CLI" ]; then
|
|
17106
17223
|
exec node "$MASSU_CLI" "$@"
|
|
17107
17224
|
fi
|
|
@@ -17149,6 +17266,7 @@ function getHookTimeout(name) {
|
|
|
17149
17266
|
function hookEntry(version, name, shim) {
|
|
17150
17267
|
const entry = { type: "command", command: hookCmd(version, name, shim), timeout: getHookTimeout(name) };
|
|
17151
17268
|
if (BACKGROUND_HOOKS.has(name)) entry.async = true;
|
|
17269
|
+
if (DECISION_HOOKS.includes(name)) entry.onFailure = "block";
|
|
17152
17270
|
return entry;
|
|
17153
17271
|
}
|
|
17154
17272
|
function buildHooksConfig(_hooksDir, shim = resolveMassuShim()) {
|
|
@@ -17263,12 +17381,33 @@ function mergeHooksConfig(existing, additions) {
|
|
|
17263
17381
|
}
|
|
17264
17382
|
merged[event] = Array.from(byMatcher.values());
|
|
17265
17383
|
}
|
|
17384
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
17385
|
+
for (const groups of Object.values(additions ?? {})) {
|
|
17386
|
+
for (const g2 of groups) {
|
|
17387
|
+
for (const h2 of g2.hooks ?? []) {
|
|
17388
|
+
const id = typeof h2?.command === "string" ? massuHookIdentity(h2.command) : null;
|
|
17389
|
+
if (id) emitted.add(id);
|
|
17390
|
+
}
|
|
17391
|
+
}
|
|
17392
|
+
}
|
|
17393
|
+
for (const event of Object.keys(merged)) {
|
|
17394
|
+
merged[event] = merged[event].map((g2) => ({
|
|
17395
|
+
...g2,
|
|
17396
|
+
hooks: (g2.hooks ?? []).filter((h2) => {
|
|
17397
|
+
const id = typeof h2?.command === "string" ? massuHookIdentity(h2.command) : null;
|
|
17398
|
+
const by = id ? SUPERSEDED_MASSU_HOOKS[id] : void 0;
|
|
17399
|
+
return !(by && emitted.has(by));
|
|
17400
|
+
})
|
|
17401
|
+
})).filter((g2) => (g2.hooks ?? []).length > 0);
|
|
17402
|
+
}
|
|
17266
17403
|
return merged;
|
|
17267
17404
|
}
|
|
17268
17405
|
function massuHookIdentity(command) {
|
|
17269
17406
|
if (!/@massu\/core|[/\\]\.?massu[/\\]/.test(command)) return null;
|
|
17270
17407
|
const m3 = /\bhook-runner\s+([A-Za-z0-9._-]+)/.exec(command);
|
|
17271
|
-
|
|
17408
|
+
if (m3) return m3[1];
|
|
17409
|
+
const legacy = /@massu[/\\]core[/\\]dist[/\\]hooks[/\\]([a-z0-9-]+)\.js/.exec(command);
|
|
17410
|
+
return legacy ? legacy[1] : null;
|
|
17272
17411
|
}
|
|
17273
17412
|
function mergeHookEntries(existing, additions) {
|
|
17274
17413
|
const seenCommands = /* @__PURE__ */ new Set();
|
|
@@ -17694,7 +17833,7 @@ async function promptStackConfirm() {
|
|
|
17694
17833
|
function capitalize(str) {
|
|
17695
17834
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
17696
17835
|
}
|
|
17697
|
-
var __filename2, __dirname2, FRAMEWORK_TO_TEMPLATE_ID, TEMPLATE_NAMES;
|
|
17836
|
+
var __filename2, __dirname2, FRAMEWORK_TO_TEMPLATE_ID, TEMPLATE_NAMES, SUPERSEDED_MASSU_HOOKS;
|
|
17698
17837
|
var init_init = __esm({
|
|
17699
17838
|
"src/commands/init.ts"() {
|
|
17700
17839
|
"use strict";
|
|
@@ -17705,6 +17844,7 @@ var init_init = __esm({
|
|
|
17705
17844
|
init_settings_local();
|
|
17706
17845
|
init_memory_path();
|
|
17707
17846
|
init_hook_timeouts();
|
|
17847
|
+
init_hook_registry();
|
|
17708
17848
|
init_detect();
|
|
17709
17849
|
init_drift();
|
|
17710
17850
|
__filename2 = fileURLToPath3(import.meta.url);
|
|
@@ -17726,6 +17866,10 @@ var init_init = __esm({
|
|
|
17726
17866
|
"swift-ios",
|
|
17727
17867
|
"multi-runtime"
|
|
17728
17868
|
];
|
|
17869
|
+
SUPERSEDED_MASSU_HOOKS = Object.freeze({
|
|
17870
|
+
"security-gate": "pre-tool-use-gate",
|
|
17871
|
+
"pre-delete-check": "pre-tool-use-gate"
|
|
17872
|
+
});
|
|
17729
17873
|
}
|
|
17730
17874
|
});
|
|
17731
17875
|
|
|
@@ -18291,41 +18435,6 @@ var init_license = __esm({
|
|
|
18291
18435
|
}
|
|
18292
18436
|
});
|
|
18293
18437
|
|
|
18294
|
-
// src/lib/hook-registry.ts
|
|
18295
|
-
function getExpectedHookFiles() {
|
|
18296
|
-
return REGISTERED_HOOKS.map((name) => `${name}.js`);
|
|
18297
|
-
}
|
|
18298
|
-
var REGISTERED_HOOKS;
|
|
18299
|
-
var init_hook_registry = __esm({
|
|
18300
|
-
"src/lib/hook-registry.ts"() {
|
|
18301
|
-
"use strict";
|
|
18302
|
-
REGISTERED_HOOKS = [
|
|
18303
|
-
"auto-learning-pipeline",
|
|
18304
|
-
"classify-failure",
|
|
18305
|
-
"cost-tracker",
|
|
18306
|
-
"fix-detector",
|
|
18307
|
-
"incident-pipeline",
|
|
18308
|
-
"intent-suggester",
|
|
18309
|
-
"memory-recall",
|
|
18310
|
-
"post-edit-context",
|
|
18311
|
-
"post-tool-use",
|
|
18312
|
-
"pre-compact",
|
|
18313
|
-
// P-E-019 (1.12.0): security-gate + pre-delete-check are kept registered
|
|
18314
|
-
// for back-compat (operator settings.json may still reference them
|
|
18315
|
-
// individually), but new installs emit only `pre-tool-use-gate` —
|
|
18316
|
-
// the consolidated single-spawn PreToolUse hook.
|
|
18317
|
-
"pre-delete-check",
|
|
18318
|
-
"pre-tool-use-gate",
|
|
18319
|
-
"quality-event",
|
|
18320
|
-
"rule-enforcement-pipeline",
|
|
18321
|
-
"security-gate",
|
|
18322
|
-
"session-end",
|
|
18323
|
-
"session-start",
|
|
18324
|
-
"user-prompt"
|
|
18325
|
-
];
|
|
18326
|
-
}
|
|
18327
|
-
});
|
|
18328
|
-
|
|
18329
18438
|
// src/preflight.ts
|
|
18330
18439
|
import { existsSync as existsSync22 } from "fs";
|
|
18331
18440
|
function assertCodegraphUsable(dbPath) {
|
|
@@ -18412,6 +18521,7 @@ var init_preflight = __esm({
|
|
|
18412
18521
|
var hook_runner_exports = {};
|
|
18413
18522
|
__export(hook_runner_exports, {
|
|
18414
18523
|
HOOK_NAME_TO_FILE: () => HOOK_NAME_TO_FILE,
|
|
18524
|
+
decisionExitCode: () => decisionExitCode,
|
|
18415
18525
|
resolveHookFile: () => resolveHookFile,
|
|
18416
18526
|
runHookRunner: () => runHookRunner
|
|
18417
18527
|
});
|
|
@@ -18443,6 +18553,16 @@ function resolveHookFile(hookName) {
|
|
|
18443
18553
|
`Hook file not found for "${hookName}". Searched: ${candidates.join(", ")}. This indicates a broken @massu/core install. Re-run \`npx -y @massu/core init\`.`
|
|
18444
18554
|
);
|
|
18445
18555
|
}
|
|
18556
|
+
function decisionExitCode(hookName, code) {
|
|
18557
|
+
if (!DECISION_HOOKS.includes(hookName) || code === 0 || code === 2) return code;
|
|
18558
|
+
process.stderr.write(
|
|
18559
|
+
`MASSU SECURITY GATE \u2014 BLOCKED
|
|
18560
|
+
|
|
18561
|
+
The ${hookName} hook ended without a verdict (exit ${code}). A gate that did not decide has not approved this call.
|
|
18562
|
+
`
|
|
18563
|
+
);
|
|
18564
|
+
return 2;
|
|
18565
|
+
}
|
|
18446
18566
|
async function runHookRunner(args2) {
|
|
18447
18567
|
const hookName = args2[0];
|
|
18448
18568
|
if (!hookName) {
|
|
@@ -18471,11 +18591,7 @@ Recognized: ${Object.keys(HOOK_NAME_TO_FILE).join(", ")}
|
|
|
18471
18591
|
env: { ...process.env, MASSU_HOOK_RUNTIME: "1" }
|
|
18472
18592
|
});
|
|
18473
18593
|
child.on("exit", (code, signal) => {
|
|
18474
|
-
|
|
18475
|
-
resolvePromise({ exitCode: 128 });
|
|
18476
|
-
return;
|
|
18477
|
-
}
|
|
18478
|
-
resolvePromise({ exitCode: code ?? 0 });
|
|
18594
|
+
resolvePromise({ exitCode: decisionExitCode(hookName, signal ? 128 : code ?? 0) });
|
|
18479
18595
|
});
|
|
18480
18596
|
child.on("error", (err) => {
|
|
18481
18597
|
process.stderr.write(`massu hook-runner: failed to spawn hook "${hookName}": ${err.message}
|
|
@@ -19293,6 +19409,10 @@ async function runInstallHooks() {
|
|
|
19293
19409
|
}
|
|
19294
19410
|
const { count } = installHooks(projectRoot);
|
|
19295
19411
|
console.log(` Installed ${count} hooks in .claude/settings.local.json`);
|
|
19412
|
+
const mcp = refreshMcpServerPin(projectRoot);
|
|
19413
|
+
if (mcp === "updated") console.log(` Updated the massu MCP server in .mcp.json to ${version} (commit this file)`);
|
|
19414
|
+
else if (mcp === "current") console.log(` .mcp.json already runs massu ${version}`);
|
|
19415
|
+
else if (mcp === "custom") console.log(" .mcp.json has a hand-written massu entry \u2014 left unchanged; check its version");
|
|
19296
19416
|
console.log("");
|
|
19297
19417
|
console.log("Hooks will activate on your next Claude Code session.");
|
|
19298
19418
|
console.log("");
|
|
@@ -33605,12 +33725,13 @@ function handleCostFeature(args2, db) {
|
|
|
33605
33725
|
ORDER BY total_cost DESC
|
|
33606
33726
|
`).all(`-${days} days`);
|
|
33607
33727
|
if (rows.length === 0) {
|
|
33608
|
-
return text7(`No feature cost data found in the last ${days} days.
|
|
33728
|
+
return text7(`No feature cost data found in the last ${days} days. Each session's priced cost is allocated across registered features (massu_sentinel_register) by the files it touched; sessions with unknown cost are skipped. Try: ${p8("cost_trend")} { days: 30 } for session-level cost trends instead.`);
|
|
33609
33729
|
}
|
|
33610
33730
|
const totalCost = rows.reduce((sum, r3) => sum + r3.total_cost, 0);
|
|
33611
33731
|
const lines = [
|
|
33612
33732
|
`## Feature Cost Attribution (${days} days)`,
|
|
33613
|
-
`Total: $${totalCost.toFixed(2)} across ${rows.length}
|
|
33733
|
+
`Total: $${totalCost.toFixed(2)} across ${rows.length} feature row(s)`,
|
|
33734
|
+
"Each session's priced cost is ALLOCATED across the registered features whose files it touched, in proportion to those touches. It is an allocation by activity, not a measured per-feature cost. Sessions with unknown cost are not included.",
|
|
33614
33735
|
"",
|
|
33615
33736
|
"| Feature | Cost | Tokens | Sessions | % of Total |",
|
|
33616
33737
|
"|---------|------|--------|----------|------------|"
|
|
@@ -2086,6 +2086,8 @@ __export(memory_db_exports, {
|
|
|
2086
2086
|
MEMORY_FILE_TITLE_LIKE: () => MEMORY_FILE_TITLE_LIKE,
|
|
2087
2087
|
MEMORY_FILE_TITLE_PREFIX: () => MEMORY_FILE_TITLE_PREFIX,
|
|
2088
2088
|
MEMORY_SCHEMA_VERSION: () => MEMORY_SCHEMA_VERSION,
|
|
2089
|
+
PENDING_SYNC_MAX_AGE_DAYS: () => PENDING_SYNC_MAX_AGE_DAYS,
|
|
2090
|
+
PENDING_SYNC_MAX_ITEMS: () => PENDING_SYNC_MAX_ITEMS,
|
|
2089
2091
|
TOOL_COST_EVENTS_RETENTION_DAYS: () => TOOL_COST_EVENTS_RETENTION_DAYS,
|
|
2090
2092
|
USAGE_COUNTER_ARMED_KEY: () => USAGE_COUNTER_ARMED_KEY,
|
|
2091
2093
|
addConversationTurn: () => addConversationTurn,
|
|
@@ -2094,6 +2096,7 @@ __export(memory_db_exports, {
|
|
|
2094
2096
|
addSummary: () => addSummary,
|
|
2095
2097
|
addToolCallDetail: () => addToolCallDetail,
|
|
2096
2098
|
addUserPrompt: () => addUserPrompt,
|
|
2099
|
+
allPendingSync: () => allPendingSync,
|
|
2097
2100
|
appendIncidentToFailureClass: () => appendIncidentToFailureClass,
|
|
2098
2101
|
armUsageCounter: () => armUsageCounter,
|
|
2099
2102
|
assignImportance: () => assignImportance,
|
|
@@ -2101,6 +2104,7 @@ __export(memory_db_exports, {
|
|
|
2101
2104
|
createSession: () => createSession,
|
|
2102
2105
|
deduplicateFailedAttempt: () => deduplicateFailedAttempt,
|
|
2103
2106
|
dequeuePendingSync: () => dequeuePendingSync,
|
|
2107
|
+
discardPendingSync: () => discardPendingSync,
|
|
2104
2108
|
embedMissingObservations: () => embedMissingObservations,
|
|
2105
2109
|
endSession: () => endSession,
|
|
2106
2110
|
enqueueRulePromotionEvent: () => enqueueRulePromotionEvent,
|
|
@@ -3083,6 +3087,35 @@ function initMemorySchema(db) {
|
|
|
3083
3087
|
}
|
|
3084
3088
|
function enqueueSyncPayload(db, payload) {
|
|
3085
3089
|
db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
|
|
3090
|
+
const overflow = db.prepare(
|
|
3091
|
+
`SELECT id, retry_count, last_error, payload FROM pending_sync
|
|
3092
|
+
WHERE created_at < datetime('now', ?)
|
|
3093
|
+
OR id NOT IN (SELECT id FROM pending_sync ORDER BY created_at DESC, id DESC LIMIT ?)
|
|
3094
|
+
LIMIT 10000`
|
|
3095
|
+
).all(`-${PENDING_SYNC_MAX_AGE_DAYS} days`, PENDING_SYNC_MAX_ITEMS);
|
|
3096
|
+
if (overflow.length > 0) {
|
|
3097
|
+
discardPendingSync(db, overflow, `over the queue limit (${PENDING_SYNC_MAX_ITEMS} items / ${PENDING_SYNC_MAX_AGE_DAYS} days)`);
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
function discardPendingSync(db, rows, why) {
|
|
3101
|
+
if (rows.length === 0) return 0;
|
|
3102
|
+
const rescued = rescueLearningFromStalePayloads(db, rows);
|
|
3103
|
+
for (let i = 0; i < rows.length; i += 500) {
|
|
3104
|
+
const ids = rows.slice(i, i + 500).map((r) => r.id);
|
|
3105
|
+
db.prepare(`DELETE FROM pending_sync WHERE id IN (${ids.map(() => "?").join(",")})`).run(...ids);
|
|
3106
|
+
}
|
|
3107
|
+
if (rescued > 0) {
|
|
3108
|
+
process.stderr.write(
|
|
3109
|
+
`[massu] RESCUED ${rescued} learned rule(s) from ${rows.length} discarded payload(s) \u2014 they were re-queued for delivery, NOT discarded.
|
|
3110
|
+
`
|
|
3111
|
+
);
|
|
3112
|
+
}
|
|
3113
|
+
process.stderr.write(`[massu] cloud sync: discarded ${rows.length} queued upload(s): ${why}.
|
|
3114
|
+
`);
|
|
3115
|
+
return rows.length;
|
|
3116
|
+
}
|
|
3117
|
+
function allPendingSync(db) {
|
|
3118
|
+
return db.prepare("SELECT id, retry_count, last_error, payload FROM pending_sync").all();
|
|
3086
3119
|
}
|
|
3087
3120
|
function getMemoryMeta(db, key) {
|
|
3088
3121
|
const row = db.prepare("SELECT value FROM memory_meta WHERE key = ?").get(key);
|
|
@@ -3285,7 +3318,9 @@ function dequeuePendingSync(db, limit = 10) {
|
|
|
3285
3318
|
}
|
|
3286
3319
|
}
|
|
3287
3320
|
return db.prepare(
|
|
3288
|
-
|
|
3321
|
+
// Least-retried first: oldest-first retried the same 10 items until they were discarded
|
|
3322
|
+
// while every newer item waited at retry 0 (2026-11-09: 43,492 never attempted).
|
|
3323
|
+
"SELECT id, payload, retry_count FROM pending_sync ORDER BY retry_count ASC, created_at ASC LIMIT ?"
|
|
3289
3324
|
).all(limit);
|
|
3290
3325
|
}
|
|
3291
3326
|
function removePendingSync(db, id) {
|
|
@@ -3958,7 +3993,7 @@ function scoreFailureClasses(db, matchText, filePath, promptContext, weights) {
|
|
|
3958
3993
|
}
|
|
3959
3994
|
return bestMatch;
|
|
3960
3995
|
}
|
|
3961
|
-
var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
|
|
3996
|
+
var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, PENDING_SYNC_MAX_ITEMS, PENDING_SYNC_MAX_AGE_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
|
|
3962
3997
|
var init_memory_db = __esm({
|
|
3963
3998
|
"src/memory-db.ts"() {
|
|
3964
3999
|
"use strict";
|
|
@@ -3973,6 +4008,8 @@ var init_memory_db = __esm({
|
|
|
3973
4008
|
init_memory_embed_sweep();
|
|
3974
4009
|
MEMORY_SCHEMA_VERSION = 2;
|
|
3975
4010
|
TOOL_COST_EVENTS_RETENTION_DAYS = 90;
|
|
4011
|
+
PENDING_SYNC_MAX_ITEMS = 500;
|
|
4012
|
+
PENDING_SYNC_MAX_AGE_DAYS = 30;
|
|
3976
4013
|
MAX_DRAFT_TEXT_LEN = 16384;
|
|
3977
4014
|
FUNNEL_EVENT_OUTBOX_CAP = 2e4;
|
|
3978
4015
|
_temporalColCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -4058,7 +4095,7 @@ var init_hook_failure_signal = __esm({
|
|
|
4058
4095
|
init_config();
|
|
4059
4096
|
import { execFileSync } from "child_process";
|
|
4060
4097
|
import { createHash } from "crypto";
|
|
4061
|
-
import { existsSync as
|
|
4098
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7, unlinkSync as unlinkSync3, readdirSync as readdirSync2, statSync as statSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync7 } from "fs";
|
|
4062
4099
|
import { tmpdir as tmpdir2 } from "os";
|
|
4063
4100
|
import { join as join6, dirname as dirname8 } from "path";
|
|
4064
4101
|
|
|
@@ -4100,20 +4137,48 @@ function writeHookContext(hookEventName, message) {
|
|
|
4100
4137
|
// src/hooks/auto-learning-pipeline.ts
|
|
4101
4138
|
init_hook_failure_signal();
|
|
4102
4139
|
|
|
4140
|
+
// src/lib/safe-write.ts
|
|
4141
|
+
import {
|
|
4142
|
+
writeFileSync as writeFileSync3,
|
|
4143
|
+
renameSync,
|
|
4144
|
+
openSync as openSync2,
|
|
4145
|
+
fsyncSync as fsyncSync2,
|
|
4146
|
+
closeSync as closeSync2,
|
|
4147
|
+
existsSync as existsSync9,
|
|
4148
|
+
realpathSync,
|
|
4149
|
+
unlinkSync as unlinkSync2,
|
|
4150
|
+
accessSync as accessSync2,
|
|
4151
|
+
statSync as statSync2,
|
|
4152
|
+
chmodSync as chmodSync3,
|
|
4153
|
+
constants
|
|
4154
|
+
} from "fs";
|
|
4155
|
+
function isSameRealPath(a, b) {
|
|
4156
|
+
try {
|
|
4157
|
+
return realpathSync(a) === realpathSync(b);
|
|
4158
|
+
} catch {
|
|
4159
|
+
return false;
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4103
4163
|
// src/hooks/lib/is-direct-invocation.ts
|
|
4104
|
-
import { pathToFileURL } from "url";
|
|
4164
|
+
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
|
|
4105
4165
|
function isDirectInvocation(moduleUrl) {
|
|
4106
4166
|
const entry = process.argv[1];
|
|
4107
4167
|
if (!entry) return false;
|
|
4108
4168
|
try {
|
|
4109
|
-
|
|
4169
|
+
if (moduleUrl === pathToFileURL(entry).href) return true;
|
|
4170
|
+
} catch {
|
|
4171
|
+
return false;
|
|
4172
|
+
}
|
|
4173
|
+
try {
|
|
4174
|
+
return isSameRealPath(fileURLToPath2(moduleUrl), entry);
|
|
4110
4175
|
} catch {
|
|
4111
4176
|
return false;
|
|
4112
4177
|
}
|
|
4113
4178
|
}
|
|
4114
4179
|
|
|
4115
4180
|
// src/hooks/lib/session-touched-files.ts
|
|
4116
|
-
import { readFileSync as readFileSync6, existsSync as
|
|
4181
|
+
import { readFileSync as readFileSync6, existsSync as existsSync10, realpathSync as realpathSync2 } from "fs";
|
|
4117
4182
|
import { relative, isAbsolute, resolve as resolve5 } from "path";
|
|
4118
4183
|
var FILE_WRITING_TOOLS = Object.freeze({
|
|
4119
4184
|
Edit: ["file_path"],
|
|
@@ -4123,7 +4188,7 @@ var FILE_WRITING_TOOLS = Object.freeze({
|
|
|
4123
4188
|
});
|
|
4124
4189
|
function canonical(p) {
|
|
4125
4190
|
try {
|
|
4126
|
-
return
|
|
4191
|
+
return realpathSync2(p);
|
|
4127
4192
|
} catch {
|
|
4128
4193
|
return p;
|
|
4129
4194
|
}
|
|
@@ -4171,7 +4236,7 @@ function transcriptTouchedFiles(root, transcriptPath) {
|
|
|
4171
4236
|
return out;
|
|
4172
4237
|
}
|
|
4173
4238
|
function flagFileTouchedFiles(root, flagPath) {
|
|
4174
|
-
if (!
|
|
4239
|
+
if (!existsSync10(flagPath)) return null;
|
|
4175
4240
|
let raw;
|
|
4176
4241
|
try {
|
|
4177
4242
|
raw = readFileSync6(flagPath, "utf-8");
|
|
@@ -4592,7 +4657,7 @@ function readShownKeys(ackPath) {
|
|
|
4592
4657
|
}
|
|
4593
4658
|
function writeShownKeys(ackPath, keys) {
|
|
4594
4659
|
mkdirSync7(dirname8(ackPath), { recursive: true });
|
|
4595
|
-
|
|
4660
|
+
writeFileSync4(ackPath, JSON.stringify({ shown: [...keys].sort() }), "utf-8");
|
|
4596
4661
|
}
|
|
4597
4662
|
function freshMatches(matchKeys, shown) {
|
|
4598
4663
|
return [...new Set(matchKeys)].filter((k) => !shown.has(k));
|
|
@@ -4602,7 +4667,7 @@ function recentIncidentFiles(dir, nowMs = Date.now()) {
|
|
|
4602
4667
|
try {
|
|
4603
4668
|
return readdirSync2(dir).filter((f) => f.endsWith(".md")).filter((f) => {
|
|
4604
4669
|
try {
|
|
4605
|
-
return nowMs -
|
|
4670
|
+
return nowMs - statSync3(join6(dir, f)).mtimeMs <= RECENT_INCIDENT_WINDOW_MS;
|
|
4606
4671
|
} catch {
|
|
4607
4672
|
return false;
|
|
4608
4673
|
}
|
|
@@ -4616,7 +4681,7 @@ function readPostImageFrom(root) {
|
|
|
4616
4681
|
return (rel) => {
|
|
4617
4682
|
try {
|
|
4618
4683
|
const abs = join6(root, rel);
|
|
4619
|
-
if (
|
|
4684
|
+
if (statSync3(abs).size > MAX_POST_IMAGE_BYTES) return null;
|
|
4620
4685
|
return readFileSync7(abs, "utf-8");
|
|
4621
4686
|
} catch {
|
|
4622
4687
|
return null;
|
|
@@ -4729,7 +4794,7 @@ async function main() {
|
|
|
4729
4794
|
const autoLearn = config.autoLearning;
|
|
4730
4795
|
const flagPath = getSessionFlagPath(hookInput.session_id);
|
|
4731
4796
|
let sessionFixes = [];
|
|
4732
|
-
if (
|
|
4797
|
+
if (existsSync11(flagPath)) {
|
|
4733
4798
|
try {
|
|
4734
4799
|
sessionFixes = readFileSync7(flagPath, "utf-8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
4735
4800
|
} catch {
|
|
@@ -4821,16 +4886,16 @@ async function main() {
|
|
|
4821
4886
|
}
|
|
4822
4887
|
function cleanup(flagPath) {
|
|
4823
4888
|
try {
|
|
4824
|
-
if (
|
|
4889
|
+
if (existsSync11(flagPath)) unlinkSync3(flagPath);
|
|
4825
4890
|
const dir = join6(tmpdir2(), "massu-auto-learning");
|
|
4826
|
-
if (
|
|
4891
|
+
if (existsSync11(dir)) {
|
|
4827
4892
|
const now = Date.now();
|
|
4828
4893
|
for (const file of readdirSync2(dir)) {
|
|
4829
4894
|
const fullPath = join6(dir, file);
|
|
4830
4895
|
try {
|
|
4831
|
-
const stat =
|
|
4896
|
+
const stat = statSync3(fullPath);
|
|
4832
4897
|
if (now - stat.mtimeMs > 864e5) {
|
|
4833
|
-
|
|
4898
|
+
unlinkSync3(fullPath);
|
|
4834
4899
|
}
|
|
4835
4900
|
} catch {
|
|
4836
4901
|
}
|