@massu/core 2.3.1 → 2.5.0
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 +131 -0
- package/dist/cli.js +175 -41
- package/dist/hooks/auto-learning-pipeline.js +16 -2
- package/dist/hooks/classify-failure.js +13 -2
- package/dist/hooks/cost-tracker.js +13 -2
- package/dist/hooks/fix-detector.js +13 -2
- package/dist/hooks/incident-pipeline.js +13 -2
- package/dist/hooks/intent-suggester.js +13 -2
- package/dist/hooks/memory-recall.js +13 -2
- package/dist/hooks/post-edit-context.js +13 -2
- package/dist/hooks/post-tool-use.js +13 -2
- package/dist/hooks/pre-compact.js +13 -2
- package/dist/hooks/pre-delete-check.js +13 -2
- package/dist/hooks/pre-tool-use-gate.js +13 -2
- package/dist/hooks/quality-event.js +13 -2
- package/dist/hooks/rule-enforcement-pipeline.js +13 -2
- package/dist/hooks/security-gate.js +13 -2
- package/dist/hooks/session-end.js +13 -2
- package/dist/hooks/session-start.js +22 -7
- package/dist/hooks/user-prompt.js +13 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,137 @@ 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.0] - 2026-08-01
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **`install-hooks` ADDED a hook registration instead of REPLACING it, so every version bump
|
|
18
|
+
doubled the hooks — and that defect BLOCKED ITS OWN REMEDY.** `mergeHookEntries` deduped by
|
|
19
|
+
EXACT COMMAND STRING, and the emitted command embeds the version, so
|
|
20
|
+
`npx -y @massu/core@1.16.2 hook-runner user-prompt` and the `@2.4.0` form were two different
|
|
21
|
+
strings, two different hooks, BOTH kept. The command was simultaneously the payload and the
|
|
22
|
+
identity, so any payload change forged a new identity.
|
|
23
|
+
|
|
24
|
+
Measured, not theoretical: one workspace carried `1.15.5` x16 **and** `1.15.2` x15 at once,
|
|
25
|
+
with 15 hook names firing TWICE per event and writing double telemetry indefinitely. Worse,
|
|
26
|
+
because upgrading duplicated, nobody upgraded — 9 of 10 workspaces on this machine sat on a
|
|
27
|
+
pre-2.0.0 version whose `better-sqlite3` ABI did not match the running Node, and one had
|
|
28
|
+
**26,119 `NODE_MODULE_VERSION` hook failures**: a total massu outage. It stayed invisible
|
|
29
|
+
because a crashing hook still **exits 0**.
|
|
30
|
+
|
|
31
|
+
Registration identity is now the `hook-runner` subcommand — a stable CLI contract (`cli.ts`
|
|
32
|
+
dispatches on exactly that token), invariant under both a version bump and a launch-mechanism
|
|
33
|
+
change. Non-massu entries keep the historical exact-command semantics, so a customer's own
|
|
34
|
+
hooks are never touched or collapsed.
|
|
35
|
+
|
|
36
|
+
Proof it can fail (CR-72, planted in the real tree, restored byte-identical by sha256):
|
|
37
|
+
reverting the identity branch to exact-command dedup turns
|
|
38
|
+
`installer-registration-identity-drift-guard.test.ts` RED — that guard drives the REAL
|
|
39
|
+
`installHooks()` over a real temp project seeded with an older-version block, a node-direct
|
|
40
|
+
block and a customer hook.
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- **A version-stable launcher shim, cutting ~0.86s of npm bootstrap off each hook fire.**
|
|
45
|
+
`npx -y @massu/core@<v> …` spends ~0.86s starting npm before any massu code runs. Measured
|
|
46
|
+
interleaved, min-of-5: `npx` 0.97s, `npx --offline` 0.88s, `node <cli.js>` 0.11s — so the
|
|
47
|
+
cost is npm's own startup, NOT the registry, and no npx flag addresses it.
|
|
48
|
+
|
|
49
|
+
`install-hooks` now writes `~/.massu/bin/massu-hook` and emits
|
|
50
|
+
`"~/.massu/bin/massu-hook" <version> hook-runner <name>`. That path is VERSION-STABLE, and
|
|
51
|
+
the shim re-resolves the runtime AT FIRE TIME, exec'ing npx when it is absent.
|
|
52
|
+
|
|
53
|
+
This deliberately does NOT bake a versioned absolute path, which would reintroduce the class
|
|
54
|
+
P-003 (v1.9.4) eliminated: such a path is "invalidated by cache clears, global-install
|
|
55
|
+
relocations, or npx upgrades — silently 404-ing the hooks". Probing at emit time says nothing
|
|
56
|
+
about invalidation after emit. Three fail-safe layers instead — shim missing at emit emits
|
|
57
|
+
npx; runtime missing at fire execs npx; both present takes the fast path — so no single
|
|
58
|
+
missing artifact is fatal and a stale registration self-heals.
|
|
59
|
+
|
|
60
|
+
Measured after: fast path **0.11s vs npx 1.07s (9.7x)**. The fallback is mutation-proven — a
|
|
61
|
+
shim with its npx branch removed FAILS the same no-runtime case (exit 27), so the test is not
|
|
62
|
+
vacuous. `exec` in both branches preserves stdin, stdout and the exit code.
|
|
63
|
+
|
|
64
|
+
Semver MINOR: `massuHookIdentity`, `massuShimPath`, `massuShimBody`, `installMassuShim`,
|
|
65
|
+
`resolveMassuShim`, `massuRuntimeDir`, `massuRuntimeCliPath`, `resolveMassuRuntimeCli` and
|
|
66
|
+
`materializeMassuRuntime` are new exports. `MASSU_NO_NODE_DIRECT=1` forces the npx form.
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
|
|
70
|
+
- **CR-70 Layer 3 amended: installer symmetry is no longer npx-specific.** Both emitters read
|
|
71
|
+
one resolver and agree on mechanism AND pinned version. The drift-guard previously asserted
|
|
72
|
+
the literal `'npx'`, which pinned an implementation detail rather than the property — it
|
|
73
|
+
would go RED on a correct mechanism change and GREEN on a wrong one that used npx on both
|
|
74
|
+
sides. Three assertions were amended on the same grounds
|
|
75
|
+
(`init-hook-paths-no-absolute`, `init-mcp-json-pin`, `cli.test.ts`); each keeps its original
|
|
76
|
+
protection of a visible pinned version and a known launch mechanism.
|
|
77
|
+
|
|
78
|
+
## [2.4.0] - 2026-07-31
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
|
|
82
|
+
- **Hooks silently lost telemetry whenever two sessions wrote at once — `db-driver.ts` never
|
|
83
|
+
armed `PRAGMA busy_timeout`, so SQLite's default of 0ms made a blocked writer fail
|
|
84
|
+
INSTANTLY.** Both engine paths (`openNodeSqlite`, `openBetterSqlite3`) opened a connection
|
|
85
|
+
and handed it out without a busy-timeout, so a writer that met a competing writer's lock
|
|
86
|
+
got `SQLITE_BUSY` ("database is locked") with no wait at all. Every hook catches that,
|
|
87
|
+
appends to `.massu/hook-failures.jsonl`, and **exits 0** — so the observation, cost, or
|
|
88
|
+
session row was gone with no failing signal anywhere. Measured before the fix across two
|
|
89
|
+
active workspaces: **2,169 lost rows in one and 156 in the other**, still accruing. In one
|
|
90
|
+
of them the throw landed in `session-start`'s `createSession`, which also skipped
|
|
91
|
+
`pullTeamPromotions` further down the same `try` — so team/enterprise promotions silently
|
|
92
|
+
stopped pulling on any session that lost the race. Concurrency is the NORMAL state whenever
|
|
93
|
+
more than one agent session shares a repo, not an edge case.
|
|
94
|
+
|
|
95
|
+
`PRAGMA busy_timeout` is set — rather than the ctor `timeout` option — because it is
|
|
96
|
+
engine-agnostic across `node:sqlite` and better-sqlite3, connection-local, and needs no
|
|
97
|
+
lock of its own. It is applied in `db-driver.ts`, the CR-69 sole open chokepoint, so
|
|
98
|
+
**every** opener (hooks, MCP server, CLI) inherits it. Default **5000ms**, overridable via
|
|
99
|
+
`MASSU_DB_BUSY_TIMEOUT_MS`; `0` is legal and restores the old fail-instantly behaviour,
|
|
100
|
+
which is how the tests prove they can still go RED. A malformed value falls back to the
|
|
101
|
+
default rather than disabling the wait (fail-safe, not fail-open).
|
|
102
|
+
|
|
103
|
+
Semver MINOR, not PATCH: `resolveBusyTimeoutMs()`, `DEFAULT_BUSY_TIMEOUT_MS` and
|
|
104
|
+
`DB_BUSY_TIMEOUT_ENV` are new exports. This release also carries the unpublished 2.3.2.
|
|
105
|
+
|
|
106
|
+
Proof it can fail (CR-72, planted in the real tree, restored byte-identical by sha256):
|
|
107
|
+
removing the pragma from `openNodeSqlite` turns the behavioural test RED
|
|
108
|
+
(`db-busy-timeout.test.ts` — a `worker_threads` lock-holder, because the plan audit proved
|
|
109
|
+
a same-thread holder is impossible: SQLite's busy-wait blocks the event loop, so the
|
|
110
|
+
release timer never fires); removing it from `openBetterSqlite3` turns drift-guard clause
|
|
111
|
+
(e) RED; hard-coding the value turns clause (e2) RED.
|
|
112
|
+
New gate registered in `scripts/lib/gate-registry.json` (416 → 417).
|
|
113
|
+
Incident: `docs/incidents/2026-07-23-hook-db-busy-timeout-silent-loss.md`.
|
|
114
|
+
(plan-2026-07-23-hook-latency-silent-loss-fixes, P1)
|
|
115
|
+
|
|
116
|
+
## [2.3.2] - 2026-07-24
|
|
117
|
+
|
|
118
|
+
### Fixed
|
|
119
|
+
|
|
120
|
+
- **`sync-public.sh` self-exempts its bulk `git rm -rf .` / `git clean -fd` from the machine
|
|
121
|
+
deletion circuit breaker.** The public-mirror sync wipes the mirror with `git rm -rf .` +
|
|
122
|
+
`git clean -fd` — hundreds of unlinks under `$HOME`, whose bulk-delete signature the breaker
|
|
123
|
+
SIGSTOPs — freezing the git child mid-sync on every run. Both bulk deletes now route through a
|
|
124
|
+
`cb_exempt_git` helper that registers each git child pid plus a start-time identity token in
|
|
125
|
+
`~/.claude/cb-exempt-pids.txt` for the duration of that one command, scoped to only the pids this
|
|
126
|
+
script adds, fail-open (the sync never depends on the breaker), with an `EXIT`/`INT`/`TERM`
|
|
127
|
+
cleanup trap that de-registers them even on interrupt. The start-time token pairs with the
|
|
128
|
+
breaker's identity-verifying exempt list so a reused pid cannot inherit a stale exemption.
|
|
129
|
+
Drift-guarded (`sync-public-cb-exempt-drift-guard.test.ts`, registered as a DEFEAT-proven G-6
|
|
130
|
+
anti-vacuity guard).
|
|
131
|
+
|
|
132
|
+
### Changed
|
|
133
|
+
|
|
134
|
+
- **Synced `packages/core/CHANGELOG.md` with the previously-missing `[2.3.1]` entry.** The 2.3.1
|
|
135
|
+
release added `[2.3.1]` to the root and website CHANGELOGs but omitted
|
|
136
|
+
`packages/core/CHANGELOG.md`, leaving the three copies out of sync. Parity restored.
|
|
137
|
+
- **Trimmed `CLAUDE.md` back under its size budget**, relocating the removed detail into
|
|
138
|
+
`.claude/reference/patterns-quickref.md` so `.claude/CLAUDE.md` stays under its 55,000-character
|
|
139
|
+
cap.
|
|
140
|
+
|
|
141
|
+
No runtime behaviour of `@massu/core` changes in this release; it is repo-tooling and documentation
|
|
142
|
+
only. Published because CR-64 forbids commits sitting on top of a published, tagged release.
|
|
143
|
+
|
|
13
144
|
## [2.3.1] - 2026-07-23
|
|
14
145
|
|
|
15
146
|
### Fixed
|
package/dist/cli.js
CHANGED
|
@@ -560,6 +560,12 @@ import { join as join2 } from "path";
|
|
|
560
560
|
function resolveDbEngine() {
|
|
561
561
|
return process.env[DB_ENGINE_ENV] === "better-sqlite3" ? "better-sqlite3" : DEFAULT_DB_ENGINE;
|
|
562
562
|
}
|
|
563
|
+
function resolveBusyTimeoutMs() {
|
|
564
|
+
const raw = process.env[DB_BUSY_TIMEOUT_ENV];
|
|
565
|
+
if (raw === void 0 || raw === "") return DEFAULT_BUSY_TIMEOUT_MS;
|
|
566
|
+
const n = Number(raw);
|
|
567
|
+
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : DEFAULT_BUSY_TIMEOUT_MS;
|
|
568
|
+
}
|
|
563
569
|
function nodeSqliteCtor() {
|
|
564
570
|
if (!_nodeCtor) _nodeCtor = req2("node:sqlite").DatabaseSync;
|
|
565
571
|
return _nodeCtor;
|
|
@@ -581,6 +587,7 @@ function openNodeSqlite(dbPath, opts) {
|
|
|
581
587
|
enableForeignKeyConstraints: true,
|
|
582
588
|
allowExtension: false
|
|
583
589
|
});
|
|
590
|
+
raw.exec(`PRAGMA busy_timeout = ${resolveBusyTimeoutMs()}`);
|
|
584
591
|
let savepointSeq = 0;
|
|
585
592
|
const wrapStmt = (sql) => {
|
|
586
593
|
const st = raw.prepare(sql);
|
|
@@ -664,7 +671,9 @@ function openNodeSqlite(dbPath, opts) {
|
|
|
664
671
|
return handle;
|
|
665
672
|
}
|
|
666
673
|
function openBetterSqlite3(dbPath, opts) {
|
|
667
|
-
|
|
674
|
+
const db = openDatabase(dbPath, opts);
|
|
675
|
+
db.exec(`PRAGMA busy_timeout = ${resolveBusyTimeoutMs()}`);
|
|
676
|
+
return db;
|
|
668
677
|
}
|
|
669
678
|
function openDatabase2(dbPath, opts = {}) {
|
|
670
679
|
return resolveDbEngine() === "better-sqlite3" ? openBetterSqlite3(dbPath, opts) : openNodeSqlite(dbPath, opts);
|
|
@@ -708,7 +717,7 @@ function probeMemoryDbUsable2(opts = {}) {
|
|
|
708
717
|
}
|
|
709
718
|
return { ok: true };
|
|
710
719
|
}
|
|
711
|
-
var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, _nodeCtor, NODE_SQLITE_REMEDY;
|
|
720
|
+
var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, DEFAULT_BUSY_TIMEOUT_MS, DB_BUSY_TIMEOUT_ENV, _nodeCtor, NODE_SQLITE_REMEDY;
|
|
712
721
|
var init_db_driver = __esm({
|
|
713
722
|
"src/db-driver.ts"() {
|
|
714
723
|
"use strict";
|
|
@@ -717,6 +726,8 @@ var init_db_driver = __esm({
|
|
|
717
726
|
req2 = createRequire2(import.meta.url);
|
|
718
727
|
DEFAULT_DB_ENGINE = "node-sqlite";
|
|
719
728
|
DB_ENGINE_ENV = "MASSU_DB_ENGINE";
|
|
729
|
+
DEFAULT_BUSY_TIMEOUT_MS = 5e3;
|
|
730
|
+
DB_BUSY_TIMEOUT_ENV = "MASSU_DB_BUSY_TIMEOUT_MS";
|
|
720
731
|
_nodeCtor = null;
|
|
721
732
|
NODE_SQLITE_REMEDY = "Node's built-in node:sqlite failed to open the database. Ensure Node >= 22.16.0 (`node --version`) and that this Node build includes SQLite, then restart your MCP client / Claude Code.";
|
|
722
733
|
}
|
|
@@ -12946,23 +12957,23 @@ function introspectPython(detection, projectRoot) {
|
|
|
12946
12957
|
for (const path of candidates) {
|
|
12947
12958
|
const body = readSafe(path);
|
|
12948
12959
|
if (body === null) continue;
|
|
12949
|
-
const authRegex =
|
|
12960
|
+
const authRegex = new RegExp(PY_AUTH_DEP_PATTERN, "gu");
|
|
12950
12961
|
forEachMatch(authRegex, body, (m3) => {
|
|
12951
12962
|
const name = m3[1];
|
|
12952
12963
|
if (!authDeps.has(name)) authDeps.set(name, path);
|
|
12953
12964
|
});
|
|
12954
|
-
const djangoAuthRegex =
|
|
12965
|
+
const djangoAuthRegex = new RegExp(PY_DJANGO_AUTH_PATTERN, "gmu");
|
|
12955
12966
|
forEachMatch(djangoAuthRegex, body, (m3) => {
|
|
12956
12967
|
const name = m3[1];
|
|
12957
12968
|
if (!authDeps.has(name)) authDeps.set(name, path);
|
|
12958
12969
|
});
|
|
12959
|
-
const prefixRegex =
|
|
12970
|
+
const prefixRegex = new RegExp(PY_API_PREFIX_PATTERN, "gu");
|
|
12960
12971
|
forEachMatch(prefixRegex, body, (m3) => {
|
|
12961
12972
|
const fullPrefix = m3[1];
|
|
12962
12973
|
const base = extractPrefixBase(fullPrefix);
|
|
12963
12974
|
if (base && !prefixBases.has(base)) prefixBases.set(base, path);
|
|
12964
12975
|
});
|
|
12965
|
-
const asyncRegex =
|
|
12976
|
+
const asyncRegex = new RegExp(PY_TEST_ASYNC_PATTERN, "gmu");
|
|
12966
12977
|
forEachMatch(asyncRegex, body, (m3) => {
|
|
12967
12978
|
const pat = m3[1].trim();
|
|
12968
12979
|
if (!testAsyncPatterns.has(pat)) testAsyncPatterns.set(pat, path);
|
|
@@ -13167,10 +13178,14 @@ function relativeTo(projectRoot, absPath) {
|
|
|
13167
13178
|
}
|
|
13168
13179
|
return basename4(absPath);
|
|
13169
13180
|
}
|
|
13170
|
-
var MAX_FILE_BYTES, MAX_SAMPLES_PER_ADAPTER, MAX_DIR_DEPTH;
|
|
13181
|
+
var PY_AUTH_DEP_PATTERN, PY_DJANGO_AUTH_PATTERN, PY_API_PREFIX_PATTERN, PY_TEST_ASYNC_PATTERN, MAX_FILE_BYTES, MAX_SAMPLES_PER_ADAPTER, MAX_DIR_DEPTH;
|
|
13171
13182
|
var init_regex_fallback = __esm({
|
|
13172
13183
|
"src/detect/regex-fallback.ts"() {
|
|
13173
13184
|
"use strict";
|
|
13185
|
+
PY_AUTH_DEP_PATTERN = String.raw`\bDepends\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)`;
|
|
13186
|
+
PY_DJANGO_AUTH_PATTERN = String.raw`^@\s*([a-z_][a-z0-9_]*(?:_required|_login))\b`;
|
|
13187
|
+
PY_API_PREFIX_PATTERN = String.raw`\bAPIRouter\s*\(\s*[^)]*?prefix\s*=\s*["']([^"']+)["']`;
|
|
13188
|
+
PY_TEST_ASYNC_PATTERN = String.raw`^(@pytest\.mark\.asyncio(?:\s*\([^)]*\))?)`;
|
|
13174
13189
|
MAX_FILE_BYTES = 256 * 1024;
|
|
13175
13190
|
MAX_SAMPLES_PER_ADAPTER = 3;
|
|
13176
13191
|
MAX_DIR_DEPTH = 6;
|
|
@@ -15965,14 +15980,23 @@ __export(init_exports, {
|
|
|
15965
15980
|
getInstallerVersion: () => getInstallerVersion,
|
|
15966
15981
|
initMemoryDir: () => initMemoryDir,
|
|
15967
15982
|
installHooks: () => installHooks,
|
|
15983
|
+
installMassuShim: () => installMassuShim,
|
|
15968
15984
|
isTemplateName: () => isTemplateName,
|
|
15969
15985
|
listTemplates: () => listTemplates,
|
|
15986
|
+
massuHookIdentity: () => massuHookIdentity,
|
|
15987
|
+
massuRuntimeCliPath: () => massuRuntimeCliPath,
|
|
15988
|
+
massuRuntimeDir: () => massuRuntimeDir,
|
|
15989
|
+
massuShimBody: () => massuShimBody,
|
|
15990
|
+
massuShimPath: () => massuShimPath,
|
|
15991
|
+
materializeMassuRuntime: () => materializeMassuRuntime,
|
|
15970
15992
|
mergeHooksConfig: () => mergeHooksConfig,
|
|
15971
15993
|
parseInitArgs: () => parseInitArgs,
|
|
15972
15994
|
printInitHelp: () => printInitHelp,
|
|
15973
15995
|
registerMcpServer: () => registerMcpServer,
|
|
15974
15996
|
renderConfigYaml: () => renderConfigYaml,
|
|
15975
15997
|
resolveHooksDir: () => resolveHooksDir,
|
|
15998
|
+
resolveMassuRuntimeCli: () => resolveMassuRuntimeCli,
|
|
15999
|
+
resolveMassuShim: () => resolveMassuShim,
|
|
15976
16000
|
resolveTemplatesDir: () => resolveTemplatesDir,
|
|
15977
16001
|
runInit: () => runInit,
|
|
15978
16002
|
validateWrittenConfig: () => validateWrittenConfig,
|
|
@@ -15982,6 +16006,7 @@ import { closeSync as closeSync4, existsSync as existsSync19, fsyncSync as fsync
|
|
|
15982
16006
|
import { resolve as resolve9, basename as basename6, dirname as dirname12 } from "path";
|
|
15983
16007
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
15984
16008
|
import { homedir as homedir7 } from "os";
|
|
16009
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
15985
16010
|
import { stringify as yamlStringify, parse as yamlParse } from "yaml";
|
|
15986
16011
|
function detectFramework(projectRoot) {
|
|
15987
16012
|
const result = {
|
|
@@ -16555,11 +16580,8 @@ function registerMcpServer(projectRoot) {
|
|
|
16555
16580
|
return false;
|
|
16556
16581
|
}
|
|
16557
16582
|
const version = getInstallerVersion();
|
|
16558
|
-
|
|
16559
|
-
|
|
16560
|
-
command: "npx",
|
|
16561
|
-
args: ["-y", `@massu/core@${version}`]
|
|
16562
|
-
};
|
|
16583
|
+
const shim = resolveMassuShim();
|
|
16584
|
+
servers.massu = shim !== null ? { type: "stdio", command: shim, args: [version] } : { type: "stdio", command: "npx", args: ["-y", `@massu/core@${version}`] };
|
|
16563
16585
|
existing.mcpServers = servers;
|
|
16564
16586
|
writeFileSync5(mcpPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
16565
16587
|
return true;
|
|
@@ -16576,7 +16598,98 @@ function resolveHooksDir() {
|
|
|
16576
16598
|
}
|
|
16577
16599
|
return "node_modules/@massu/core/dist/hooks";
|
|
16578
16600
|
}
|
|
16579
|
-
function
|
|
16601
|
+
function massuRuntimeDir(version) {
|
|
16602
|
+
return resolve9(homedir7(), ".massu", "runtime", version);
|
|
16603
|
+
}
|
|
16604
|
+
function massuRuntimeCliPath(version) {
|
|
16605
|
+
return resolve9(massuRuntimeDir(version), "node_modules", "@massu", "core", "dist", "cli.js");
|
|
16606
|
+
}
|
|
16607
|
+
function resolveMassuRuntimeCli(version) {
|
|
16608
|
+
if (process.env.MASSU_NO_NODE_DIRECT === "1") return null;
|
|
16609
|
+
const cli = massuRuntimeCliPath(version);
|
|
16610
|
+
if (!existsSync19(cli)) return null;
|
|
16611
|
+
try {
|
|
16612
|
+
execFileSync2(process.execPath, [cli, "--version"], { stdio: "ignore", timeout: 2e4 });
|
|
16613
|
+
return cli;
|
|
16614
|
+
} catch {
|
|
16615
|
+
return null;
|
|
16616
|
+
}
|
|
16617
|
+
}
|
|
16618
|
+
function materializeMassuRuntime(version) {
|
|
16619
|
+
if (process.env.MASSU_NO_NODE_DIRECT === "1") return false;
|
|
16620
|
+
if (resolveMassuRuntimeCli(version) !== null) return true;
|
|
16621
|
+
const dir = massuRuntimeDir(version);
|
|
16622
|
+
try {
|
|
16623
|
+
mkdirSync10(dir, { recursive: true });
|
|
16624
|
+
execFileSync2(
|
|
16625
|
+
"npm",
|
|
16626
|
+
["install", "--prefix", dir, `@massu/core@${version}`, "--no-audit", "--no-fund", "--loglevel=error"],
|
|
16627
|
+
{ stdio: "ignore", timeout: 3e5 }
|
|
16628
|
+
);
|
|
16629
|
+
} catch {
|
|
16630
|
+
return false;
|
|
16631
|
+
}
|
|
16632
|
+
return resolveMassuRuntimeCli(version) !== null;
|
|
16633
|
+
}
|
|
16634
|
+
function massuShimPath() {
|
|
16635
|
+
return resolve9(homedir7(), ".massu", "bin", "massu-hook");
|
|
16636
|
+
}
|
|
16637
|
+
function massuShimBody() {
|
|
16638
|
+
return `#!/bin/sh
|
|
16639
|
+
# massu launcher shim \u2014 GENERATED, do not edit. Recreated by \`massu install-hooks\`.
|
|
16640
|
+
#
|
|
16641
|
+
# usage: massu-hook <version> <subcommand...>
|
|
16642
|
+
#
|
|
16643
|
+
# Resolves the massu runtime AT FIRE TIME so a deleted/moved runtime degrades to npx
|
|
16644
|
+
# instead of silently 404-ing the hook (the P-003 failure class). exec preserves stdin,
|
|
16645
|
+
# stdout, stderr and the exit code.
|
|
16646
|
+
set -u
|
|
16647
|
+
if [ "$#" -lt 1 ]; then
|
|
16648
|
+
echo "massu-hook: missing <version> argument" >&2
|
|
16649
|
+
exit 2
|
|
16650
|
+
fi
|
|
16651
|
+
MASSU_VERSION="$1"
|
|
16652
|
+
shift
|
|
16653
|
+
MASSU_CLI="\${HOME}/.massu/runtime/\${MASSU_VERSION}/node_modules/@massu/core/dist/cli.js"
|
|
16654
|
+
if [ -f "$MASSU_CLI" ]; then
|
|
16655
|
+
exec node "$MASSU_CLI" "$@"
|
|
16656
|
+
fi
|
|
16657
|
+
exec npx -y "@massu/core@\${MASSU_VERSION}" "$@"
|
|
16658
|
+
`;
|
|
16659
|
+
}
|
|
16660
|
+
function installMassuShim() {
|
|
16661
|
+
if (process.env.MASSU_NO_NODE_DIRECT === "1") return false;
|
|
16662
|
+
if (process.platform === "win32") return false;
|
|
16663
|
+
const shim = massuShimPath();
|
|
16664
|
+
try {
|
|
16665
|
+
mkdirSync10(dirname12(shim), { recursive: true });
|
|
16666
|
+
writeFileSync5(shim, massuShimBody(), "utf-8");
|
|
16667
|
+
chmodSync5(shim, 493);
|
|
16668
|
+
} catch {
|
|
16669
|
+
return false;
|
|
16670
|
+
}
|
|
16671
|
+
try {
|
|
16672
|
+
execFileSync2(shim, [], { stdio: "ignore", timeout: 1e4 });
|
|
16673
|
+
return false;
|
|
16674
|
+
} catch (e2) {
|
|
16675
|
+
const status = e2.status;
|
|
16676
|
+
return status === 2;
|
|
16677
|
+
}
|
|
16678
|
+
}
|
|
16679
|
+
function resolveMassuShim() {
|
|
16680
|
+
if (process.env.MASSU_NO_NODE_DIRECT === "1") return null;
|
|
16681
|
+
if (process.platform === "win32") return null;
|
|
16682
|
+
const shim = massuShimPath();
|
|
16683
|
+
if (!existsSync19(shim)) return null;
|
|
16684
|
+
try {
|
|
16685
|
+
execFileSync2(shim, [], { stdio: "ignore", timeout: 1e4 });
|
|
16686
|
+
return null;
|
|
16687
|
+
} catch (e2) {
|
|
16688
|
+
return e2.status === 2 ? shim : null;
|
|
16689
|
+
}
|
|
16690
|
+
}
|
|
16691
|
+
function hookCmd(version, hookName, shim) {
|
|
16692
|
+
if (shim !== null) return `"${shim}" ${version} hook-runner ${hookName}`;
|
|
16580
16693
|
return `npx -y @massu/core@${version} hook-runner ${hookName}`;
|
|
16581
16694
|
}
|
|
16582
16695
|
function getHookTimeout(name) {
|
|
@@ -16584,11 +16697,12 @@ function getHookTimeout(name) {
|
|
|
16584
16697
|
}
|
|
16585
16698
|
function buildHooksConfig(_hooksDir) {
|
|
16586
16699
|
const version = getInstallerVersion();
|
|
16700
|
+
const shim = resolveMassuShim();
|
|
16587
16701
|
return {
|
|
16588
16702
|
SessionStart: [
|
|
16589
16703
|
{
|
|
16590
16704
|
hooks: [
|
|
16591
|
-
{ type: "command", command: hookCmd(version, "session-start"), timeout: getHookTimeout("session-start") }
|
|
16705
|
+
{ type: "command", command: hookCmd(version, "session-start", shim), timeout: getHookTimeout("session-start") }
|
|
16592
16706
|
]
|
|
16593
16707
|
}
|
|
16594
16708
|
],
|
|
@@ -16603,26 +16717,26 @@ function buildHooksConfig(_hooksDir) {
|
|
|
16603
16717
|
{
|
|
16604
16718
|
matcher: "Bash|Write|Edit",
|
|
16605
16719
|
hooks: [
|
|
16606
|
-
{ type: "command", command: hookCmd(version, "pre-tool-use-gate"), timeout: getHookTimeout("pre-tool-use-gate") }
|
|
16720
|
+
{ type: "command", command: hookCmd(version, "pre-tool-use-gate", shim), timeout: getHookTimeout("pre-tool-use-gate") }
|
|
16607
16721
|
]
|
|
16608
16722
|
}
|
|
16609
16723
|
],
|
|
16610
16724
|
PostToolUse: [
|
|
16611
16725
|
{
|
|
16612
16726
|
hooks: [
|
|
16613
|
-
{ type: "command", command: hookCmd(version, "post-tool-use"), timeout: getHookTimeout("post-tool-use") },
|
|
16614
|
-
{ type: "command", command: hookCmd(version, "quality-event"), timeout: getHookTimeout("quality-event") },
|
|
16615
|
-
{ type: "command", command: hookCmd(version, "cost-tracker"), timeout: getHookTimeout("cost-tracker") }
|
|
16727
|
+
{ type: "command", command: hookCmd(version, "post-tool-use", shim), timeout: getHookTimeout("post-tool-use") },
|
|
16728
|
+
{ type: "command", command: hookCmd(version, "quality-event", shim), timeout: getHookTimeout("quality-event") },
|
|
16729
|
+
{ type: "command", command: hookCmd(version, "cost-tracker", shim), timeout: getHookTimeout("cost-tracker") }
|
|
16616
16730
|
]
|
|
16617
16731
|
},
|
|
16618
16732
|
{
|
|
16619
16733
|
matcher: "Edit|Write",
|
|
16620
16734
|
hooks: [
|
|
16621
|
-
{ type: "command", command: hookCmd(version, "post-edit-context"), timeout: getHookTimeout("post-edit-context") },
|
|
16735
|
+
{ type: "command", command: hookCmd(version, "post-edit-context", shim), timeout: getHookTimeout("post-edit-context") },
|
|
16622
16736
|
// Auto-learning pipeline — classifies failures and detects fixes on
|
|
16623
16737
|
// file changes. See Phase 5-6 of the autodetect plan.
|
|
16624
|
-
{ type: "command", command: hookCmd(version, "fix-detector"), timeout: getHookTimeout("fix-detector") },
|
|
16625
|
-
{ type: "command", command: hookCmd(version, "classify-failure"), timeout: getHookTimeout("classify-failure") }
|
|
16738
|
+
{ type: "command", command: hookCmd(version, "fix-detector", shim), timeout: getHookTimeout("fix-detector") },
|
|
16739
|
+
{ type: "command", command: hookCmd(version, "classify-failure", shim), timeout: getHookTimeout("classify-failure") }
|
|
16626
16740
|
]
|
|
16627
16741
|
},
|
|
16628
16742
|
{
|
|
@@ -16630,35 +16744,35 @@ function buildHooksConfig(_hooksDir) {
|
|
|
16630
16744
|
hooks: [
|
|
16631
16745
|
// Incident + rule enforcement pipelines fire on Write-only (incidents
|
|
16632
16746
|
// are authored as .md files; rules are enforced after new-file drops).
|
|
16633
|
-
{ type: "command", command: hookCmd(version, "incident-pipeline"), timeout: getHookTimeout("incident-pipeline") },
|
|
16634
|
-
{ type: "command", command: hookCmd(version, "rule-enforcement-pipeline"), timeout: getHookTimeout("rule-enforcement-pipeline") }
|
|
16747
|
+
{ type: "command", command: hookCmd(version, "incident-pipeline", shim), timeout: getHookTimeout("incident-pipeline") },
|
|
16748
|
+
{ type: "command", command: hookCmd(version, "rule-enforcement-pipeline", shim), timeout: getHookTimeout("rule-enforcement-pipeline") }
|
|
16635
16749
|
]
|
|
16636
16750
|
}
|
|
16637
16751
|
],
|
|
16638
16752
|
Stop: [
|
|
16639
16753
|
{
|
|
16640
16754
|
hooks: [
|
|
16641
|
-
{ type: "command", command: hookCmd(version, "session-end"), timeout: getHookTimeout("session-end") },
|
|
16755
|
+
{ type: "command", command: hookCmd(version, "session-end", shim), timeout: getHookTimeout("session-end") },
|
|
16642
16756
|
// Session-end auto-learning aggregation (failure-class roll-up).
|
|
16643
|
-
{ type: "command", command: hookCmd(version, "auto-learning-pipeline"), timeout: getHookTimeout("auto-learning-pipeline") }
|
|
16757
|
+
{ type: "command", command: hookCmd(version, "auto-learning-pipeline", shim), timeout: getHookTimeout("auto-learning-pipeline") }
|
|
16644
16758
|
]
|
|
16645
16759
|
}
|
|
16646
16760
|
],
|
|
16647
16761
|
PreCompact: [
|
|
16648
16762
|
{
|
|
16649
16763
|
hooks: [
|
|
16650
|
-
{ type: "command", command: hookCmd(version, "pre-compact"), timeout: getHookTimeout("pre-compact") }
|
|
16764
|
+
{ type: "command", command: hookCmd(version, "pre-compact", shim), timeout: getHookTimeout("pre-compact") }
|
|
16651
16765
|
]
|
|
16652
16766
|
}
|
|
16653
16767
|
],
|
|
16654
16768
|
UserPromptSubmit: [
|
|
16655
16769
|
{
|
|
16656
16770
|
hooks: [
|
|
16657
|
-
{ type: "command", command: hookCmd(version, "user-prompt"), timeout: getHookTimeout("user-prompt") },
|
|
16658
|
-
{ type: "command", command: hookCmd(version, "intent-suggester"), timeout: getHookTimeout("intent-suggester") },
|
|
16771
|
+
{ type: "command", command: hookCmd(version, "user-prompt", shim), timeout: getHookTimeout("user-prompt") },
|
|
16772
|
+
{ type: "command", command: hookCmd(version, "intent-suggester", shim), timeout: getHookTimeout("intent-suggester") },
|
|
16659
16773
|
// plan-living-memory-slice-1 P3-002: automatic relevant-recall.
|
|
16660
16774
|
// Injects a compact "🧠 Relevant memory" block; fails open (empty).
|
|
16661
|
-
{ type: "command", command: hookCmd(version, "memory-recall"), timeout: getHookTimeout("memory-recall") }
|
|
16775
|
+
{ type: "command", command: hookCmd(version, "memory-recall", shim), timeout: getHookTimeout("memory-recall") }
|
|
16662
16776
|
]
|
|
16663
16777
|
}
|
|
16664
16778
|
]
|
|
@@ -16696,21 +16810,31 @@ function mergeHooksConfig(existing, additions) {
|
|
|
16696
16810
|
}
|
|
16697
16811
|
return merged;
|
|
16698
16812
|
}
|
|
16813
|
+
function massuHookIdentity(command) {
|
|
16814
|
+
if (!/@massu\/core|[/\\]\.?massu[/\\]/.test(command)) return null;
|
|
16815
|
+
const m3 = /\bhook-runner\s+([A-Za-z0-9._-]+)/.exec(command);
|
|
16816
|
+
return m3 ? m3[1] : null;
|
|
16817
|
+
}
|
|
16699
16818
|
function mergeHookEntries(existing, additions) {
|
|
16700
|
-
const
|
|
16819
|
+
const seenCommands = /* @__PURE__ */ new Set();
|
|
16820
|
+
const seenIdentities = /* @__PURE__ */ new Set();
|
|
16701
16821
|
const result = [];
|
|
16702
|
-
|
|
16703
|
-
if (!entry || typeof entry.command !== "string")
|
|
16704
|
-
|
|
16705
|
-
|
|
16706
|
-
|
|
16707
|
-
|
|
16708
|
-
|
|
16709
|
-
|
|
16710
|
-
|
|
16711
|
-
|
|
16822
|
+
const take = (entry) => {
|
|
16823
|
+
if (!entry || typeof entry.command !== "string") return;
|
|
16824
|
+
const identity = massuHookIdentity(entry.command);
|
|
16825
|
+
if (identity !== null) {
|
|
16826
|
+
if (seenIdentities.has(identity)) return;
|
|
16827
|
+
seenIdentities.add(identity);
|
|
16828
|
+
seenCommands.add(entry.command);
|
|
16829
|
+
result.push(entry);
|
|
16830
|
+
return;
|
|
16831
|
+
}
|
|
16832
|
+
if (seenCommands.has(entry.command)) return;
|
|
16833
|
+
seenCommands.add(entry.command);
|
|
16712
16834
|
result.push(entry);
|
|
16713
|
-
}
|
|
16835
|
+
};
|
|
16836
|
+
for (const entry of additions ?? []) take(entry);
|
|
16837
|
+
for (const entry of existing ?? []) take(entry);
|
|
16714
16838
|
return result;
|
|
16715
16839
|
}
|
|
16716
16840
|
function installHooks(projectRoot) {
|
|
@@ -18701,6 +18825,16 @@ async function runInstallHooks() {
|
|
|
18701
18825
|
console.log("Massu AI - Hook Installation");
|
|
18702
18826
|
console.log("============================");
|
|
18703
18827
|
console.log("");
|
|
18828
|
+
const version = getInstallerVersion();
|
|
18829
|
+
const shimOk = installMassuShim();
|
|
18830
|
+
const runtimeOk = materializeMassuRuntime(version);
|
|
18831
|
+
if (shimOk && runtimeOk) {
|
|
18832
|
+
console.log(" Launcher: shim + runtime ready (fast path, ~9x cheaper per hook)");
|
|
18833
|
+
} else if (shimOk) {
|
|
18834
|
+
console.log(" Launcher: shim ready, runtime unavailable \u2014 shim will fall back to npx at fire time");
|
|
18835
|
+
} else {
|
|
18836
|
+
console.log(" Launcher: npx (shim unavailable \u2014 works, ~0.9s slower per hook)");
|
|
18837
|
+
}
|
|
18704
18838
|
const { count } = installHooks(projectRoot);
|
|
18705
18839
|
console.log(` Installed ${count} hooks in .claude/settings.local.json`);
|
|
18706
18840
|
console.log("");
|
|
@@ -1187,6 +1187,12 @@ import { join as join2 } from "path";
|
|
|
1187
1187
|
function resolveDbEngine() {
|
|
1188
1188
|
return process.env[DB_ENGINE_ENV] === "better-sqlite3" ? "better-sqlite3" : DEFAULT_DB_ENGINE;
|
|
1189
1189
|
}
|
|
1190
|
+
function resolveBusyTimeoutMs() {
|
|
1191
|
+
const raw = process.env[DB_BUSY_TIMEOUT_ENV];
|
|
1192
|
+
if (raw === void 0 || raw === "") return DEFAULT_BUSY_TIMEOUT_MS;
|
|
1193
|
+
const n = Number(raw);
|
|
1194
|
+
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : DEFAULT_BUSY_TIMEOUT_MS;
|
|
1195
|
+
}
|
|
1190
1196
|
function nodeSqliteCtor() {
|
|
1191
1197
|
if (!_nodeCtor) _nodeCtor = req2("node:sqlite").DatabaseSync;
|
|
1192
1198
|
return _nodeCtor;
|
|
@@ -1208,6 +1214,7 @@ function openNodeSqlite(dbPath, opts) {
|
|
|
1208
1214
|
enableForeignKeyConstraints: true,
|
|
1209
1215
|
allowExtension: false
|
|
1210
1216
|
});
|
|
1217
|
+
raw.exec(`PRAGMA busy_timeout = ${resolveBusyTimeoutMs()}`);
|
|
1211
1218
|
let savepointSeq = 0;
|
|
1212
1219
|
const wrapStmt = (sql) => {
|
|
1213
1220
|
const st = raw.prepare(sql);
|
|
@@ -1291,12 +1298,14 @@ function openNodeSqlite(dbPath, opts) {
|
|
|
1291
1298
|
return handle;
|
|
1292
1299
|
}
|
|
1293
1300
|
function openBetterSqlite3(dbPath, opts) {
|
|
1294
|
-
|
|
1301
|
+
const db = openDatabase(dbPath, opts);
|
|
1302
|
+
db.exec(`PRAGMA busy_timeout = ${resolveBusyTimeoutMs()}`);
|
|
1303
|
+
return db;
|
|
1295
1304
|
}
|
|
1296
1305
|
function openDatabase2(dbPath, opts = {}) {
|
|
1297
1306
|
return resolveDbEngine() === "better-sqlite3" ? openBetterSqlite3(dbPath, opts) : openNodeSqlite(dbPath, opts);
|
|
1298
1307
|
}
|
|
1299
|
-
var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, _nodeCtor;
|
|
1308
|
+
var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, DEFAULT_BUSY_TIMEOUT_MS, DB_BUSY_TIMEOUT_ENV, _nodeCtor;
|
|
1300
1309
|
var init_db_driver = __esm({
|
|
1301
1310
|
"src/db-driver.ts"() {
|
|
1302
1311
|
"use strict";
|
|
@@ -1305,6 +1314,8 @@ var init_db_driver = __esm({
|
|
|
1305
1314
|
req2 = createRequire2(import.meta.url);
|
|
1306
1315
|
DEFAULT_DB_ENGINE = "node-sqlite";
|
|
1307
1316
|
DB_ENGINE_ENV = "MASSU_DB_ENGINE";
|
|
1317
|
+
DEFAULT_BUSY_TIMEOUT_MS = 5e3;
|
|
1318
|
+
DB_BUSY_TIMEOUT_ENV = "MASSU_DB_BUSY_TIMEOUT_MS";
|
|
1308
1319
|
_nodeCtor = null;
|
|
1309
1320
|
}
|
|
1310
1321
|
});
|
|
@@ -4132,3 +4143,6 @@ function readStdin() {
|
|
|
4132
4143
|
});
|
|
4133
4144
|
}
|
|
4134
4145
|
main();
|
|
4146
|
+
export {
|
|
4147
|
+
MAX_FULL_DIFF_BYTES
|
|
4148
|
+
};
|
|
@@ -1187,6 +1187,12 @@ import { join as join2 } from "path";
|
|
|
1187
1187
|
function resolveDbEngine() {
|
|
1188
1188
|
return process.env[DB_ENGINE_ENV] === "better-sqlite3" ? "better-sqlite3" : DEFAULT_DB_ENGINE;
|
|
1189
1189
|
}
|
|
1190
|
+
function resolveBusyTimeoutMs() {
|
|
1191
|
+
const raw = process.env[DB_BUSY_TIMEOUT_ENV];
|
|
1192
|
+
if (raw === void 0 || raw === "") return DEFAULT_BUSY_TIMEOUT_MS;
|
|
1193
|
+
const n = Number(raw);
|
|
1194
|
+
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : DEFAULT_BUSY_TIMEOUT_MS;
|
|
1195
|
+
}
|
|
1190
1196
|
function nodeSqliteCtor() {
|
|
1191
1197
|
if (!_nodeCtor) _nodeCtor = req2("node:sqlite").DatabaseSync;
|
|
1192
1198
|
return _nodeCtor;
|
|
@@ -1208,6 +1214,7 @@ function openNodeSqlite(dbPath, opts) {
|
|
|
1208
1214
|
enableForeignKeyConstraints: true,
|
|
1209
1215
|
allowExtension: false
|
|
1210
1216
|
});
|
|
1217
|
+
raw.exec(`PRAGMA busy_timeout = ${resolveBusyTimeoutMs()}`);
|
|
1211
1218
|
let savepointSeq = 0;
|
|
1212
1219
|
const wrapStmt = (sql) => {
|
|
1213
1220
|
const st = raw.prepare(sql);
|
|
@@ -1291,12 +1298,14 @@ function openNodeSqlite(dbPath, opts) {
|
|
|
1291
1298
|
return handle;
|
|
1292
1299
|
}
|
|
1293
1300
|
function openBetterSqlite3(dbPath, opts) {
|
|
1294
|
-
|
|
1301
|
+
const db = openDatabase(dbPath, opts);
|
|
1302
|
+
db.exec(`PRAGMA busy_timeout = ${resolveBusyTimeoutMs()}`);
|
|
1303
|
+
return db;
|
|
1295
1304
|
}
|
|
1296
1305
|
function openDatabase2(dbPath, opts = {}) {
|
|
1297
1306
|
return resolveDbEngine() === "better-sqlite3" ? openBetterSqlite3(dbPath, opts) : openNodeSqlite(dbPath, opts);
|
|
1298
1307
|
}
|
|
1299
|
-
var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, _nodeCtor;
|
|
1308
|
+
var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, DEFAULT_BUSY_TIMEOUT_MS, DB_BUSY_TIMEOUT_ENV, _nodeCtor;
|
|
1300
1309
|
var init_db_driver = __esm({
|
|
1301
1310
|
"src/db-driver.ts"() {
|
|
1302
1311
|
"use strict";
|
|
@@ -1305,6 +1314,8 @@ var init_db_driver = __esm({
|
|
|
1305
1314
|
req2 = createRequire2(import.meta.url);
|
|
1306
1315
|
DEFAULT_DB_ENGINE = "node-sqlite";
|
|
1307
1316
|
DB_ENGINE_ENV = "MASSU_DB_ENGINE";
|
|
1317
|
+
DEFAULT_BUSY_TIMEOUT_MS = 5e3;
|
|
1318
|
+
DB_BUSY_TIMEOUT_ENV = "MASSU_DB_BUSY_TIMEOUT_MS";
|
|
1308
1319
|
_nodeCtor = null;
|
|
1309
1320
|
}
|
|
1310
1321
|
});
|