@massu/core 1.10.7 → 1.12.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 +1303 -0
- package/dist/adapter.js +6 -4
- package/dist/cli.js +559 -400
- package/dist/detect/adapters/.bundle-shasums.json +4 -7
- package/dist/detect/adapters/rails.js +6 -4
- package/dist/detect/adapters/spring.js +6 -4
- package/dist/detect/adapters/tree-sitter-loader.js +6 -4
- package/dist/hooks/auto-learning-pipeline.js +24 -1
- package/dist/hooks/classify-failure.js +32 -3
- package/dist/hooks/cost-tracker.js +24 -0
- package/dist/hooks/fix-detector.js +97 -1
- package/dist/hooks/incident-pipeline.js +30 -1
- package/dist/hooks/intent-suggester.js +6 -3
- package/dist/hooks/post-edit-context.js +24 -1
- package/dist/hooks/post-tool-use.js +66 -18
- package/dist/hooks/pre-compact.js +27 -2
- package/dist/hooks/pre-delete-check.js +48 -22
- package/dist/hooks/pre-tool-use-gate.js +962 -0
- package/dist/hooks/quality-event.js +24 -0
- package/dist/hooks/rule-enforcement-pipeline.js +24 -1
- package/dist/hooks/security-gate.js +29 -16
- package/dist/hooks/session-end.js +24 -0
- package/dist/hooks/session-start.js +8710 -8460
- package/dist/hooks/user-prompt.js +24 -0
- package/package.json +15 -18
- package/commands/README.md +0 -134
- package/src/adapter.ts +0 -121
- package/src/adr-generator.ts +0 -292
- package/src/analytics.ts +0 -373
- package/src/audit-trail.ts +0 -450
- package/src/backfill-sessions.ts +0 -182
- package/src/changelog-generator.ts +0 -178
- package/src/cli.ts +0 -246
- package/src/cloud-sync.ts +0 -234
- package/src/commands/adapters.ts +0 -824
- package/src/commands/changelog.ts +0 -165
- package/src/commands/config-check-drift.ts +0 -133
- package/src/commands/config-refresh.ts +0 -396
- package/src/commands/config-upgrade.ts +0 -127
- package/src/commands/doctor.ts +0 -485
- package/src/commands/hook-runner.ts +0 -145
- package/src/commands/init.ts +0 -1823
- package/src/commands/install-commands.ts +0 -692
- package/src/commands/install-hooks.ts +0 -27
- package/src/commands/permissions.ts +0 -150
- package/src/commands/refresh-log.ts +0 -37
- package/src/commands/show-template.ts +0 -65
- package/src/commands/template-engine.ts +0 -301
- package/src/commands/watch.ts +0 -430
- package/src/config.ts +0 -765
- package/src/cost-tracker.ts +0 -355
- package/src/db.ts +0 -370
- package/src/dependency-scorer.ts +0 -337
- package/src/detect/adapters/aspnet.ts +0 -4
- package/src/detect/adapters/discover.ts +0 -469
- package/src/detect/adapters/file-sampler.ts +0 -225
- package/src/detect/adapters/go-chi.ts +0 -4
- package/src/detect/adapters/index.ts +0 -51
- package/src/detect/adapters/nextjs-trpc.ts +0 -166
- package/src/detect/adapters/parse-guard.ts +0 -133
- package/src/detect/adapters/phoenix.ts +0 -4
- package/src/detect/adapters/python-django.ts +0 -208
- package/src/detect/adapters/python-fastapi.ts +0 -223
- package/src/detect/adapters/python-flask.ts +0 -235
- package/src/detect/adapters/query-helpers.ts +0 -170
- package/src/detect/adapters/rails.ts +0 -4
- package/src/detect/adapters/runner.ts +0 -284
- package/src/detect/adapters/spring.ts +0 -4
- package/src/detect/adapters/swift-swiftui.ts +0 -171
- package/src/detect/adapters/tree-sitter-loader.ts +0 -517
- package/src/detect/adapters/types.ts +0 -191
- package/src/detect/codebase-introspector.ts +0 -210
- package/src/detect/domain-inferrer.ts +0 -197
- package/src/detect/drift.ts +0 -199
- package/src/detect/framework-detector.ts +0 -307
- package/src/detect/index.ts +0 -209
- package/src/detect/manifest-registry.ts +0 -261
- package/src/detect/migrate.ts +0 -362
- package/src/detect/monorepo-detector.ts +0 -348
- package/src/detect/package-detector.ts +0 -828
- package/src/detect/passthrough.ts +0 -108
- package/src/detect/regex-fallback.ts +0 -449
- package/src/detect/source-dir-detector.ts +0 -297
- package/src/detect/vr-command-map.ts +0 -167
- package/src/docs-map.json +0 -100
- package/src/docs-tools.ts +0 -519
- package/src/domains.ts +0 -182
- package/src/hooks/auto-learning-pipeline.ts +0 -228
- package/src/hooks/classify-failure.ts +0 -259
- package/src/hooks/cost-tracker.ts +0 -66
- package/src/hooks/fix-detector.ts +0 -186
- package/src/hooks/incident-pipeline.ts +0 -190
- package/src/hooks/intent-suggester.ts +0 -131
- package/src/hooks/post-edit-context.ts +0 -91
- package/src/hooks/post-tool-use.ts +0 -413
- package/src/hooks/pre-compact.ts +0 -168
- package/src/hooks/pre-delete-check.ts +0 -243
- package/src/hooks/quality-event.ts +0 -127
- package/src/hooks/rule-enforcement-pipeline.ts +0 -159
- package/src/hooks/security-gate.ts +0 -153
- package/src/hooks/session-end.ts +0 -467
- package/src/hooks/session-start.ts +0 -436
- package/src/hooks/user-prompt.ts +0 -157
- package/src/import-resolver.ts +0 -226
- package/src/knowledge-db.ts +0 -176
- package/src/knowledge-indexer.ts +0 -708
- package/src/knowledge-tools.ts +0 -1414
- package/src/lib/fileLock.ts +0 -203
- package/src/lib/gitToplevel.ts +0 -22
- package/src/lib/hook-registry.ts +0 -43
- package/src/lib/installLock.ts +0 -66
- package/src/lib/memory-path.ts +0 -49
- package/src/lib/pidLiveness.ts +0 -67
- package/src/lib/settings-local.ts +0 -110
- package/src/lib/sql-table-names.ts +0 -71
- package/src/license.ts +0 -547
- package/src/lsp/auto-detect.ts +0 -98
- package/src/lsp/client.ts +0 -776
- package/src/lsp/enrich.ts +0 -127
- package/src/lsp/types.ts +0 -221
- package/src/memory-db.ts +0 -1614
- package/src/memory-file-ingest.ts +0 -128
- package/src/memory-tools.ts +0 -424
- package/src/middleware-tree.ts +0 -71
- package/src/observability-tools.ts +0 -343
- package/src/observation-extractor.ts +0 -418
- package/src/page-deps.ts +0 -285
- package/src/permissions.ts +0 -422
- package/src/prompt-analyzer.ts +0 -332
- package/src/python/coupling-detector.ts +0 -125
- package/src/python/domain-enforcer.ts +0 -84
- package/src/python/impact-analyzer.ts +0 -96
- package/src/python/import-parser.ts +0 -244
- package/src/python/import-resolver.ts +0 -136
- package/src/python/migration-indexer.ts +0 -116
- package/src/python/migration-parser.ts +0 -332
- package/src/python/model-indexer.ts +0 -71
- package/src/python/model-parser.ts +0 -279
- package/src/python/route-indexer.ts +0 -59
- package/src/python/route-parser.ts +0 -317
- package/src/python-tools.ts +0 -630
- package/src/regression-detector.ts +0 -319
- package/src/rules.ts +0 -57
- package/src/schema-mapper.ts +0 -232
- package/src/security/adapter-origin.ts +0 -130
- package/src/security/adapter-verifier.ts +0 -319
- package/src/security/atomic-write.ts +0 -164
- package/src/security/fetcher.ts +0 -200
- package/src/security/install-tracking.ts +0 -319
- package/src/security/license-pubkey.generated.ts +0 -30
- package/src/security/license-response-verifier.ts +0 -149
- package/src/security/local-fingerprint.ts +0 -225
- package/src/security/manifest-cache.ts +0 -333
- package/src/security/manifest-schema.ts +0 -129
- package/src/security/registry-pubkey.generated.ts +0 -35
- package/src/security/telemetry.ts +0 -320
- package/src/security-scorer.ts +0 -405
- package/src/security-utils.ts +0 -133
- package/src/sentinel-db.ts +0 -580
- package/src/sentinel-scanner.ts +0 -406
- package/src/sentinel-tools.ts +0 -513
- package/src/sentinel-types.ts +0 -140
- package/src/server-dispatch.ts +0 -225
- package/src/server.ts +0 -107
- package/src/session-archiver.ts +0 -111
- package/src/session-state-generator.ts +0 -174
- package/src/team-knowledge.ts +0 -407
- package/src/tool-db-needs.ts +0 -232
- package/src/tools.ts +0 -1205
- package/src/transcript-parser.ts +0 -458
- package/src/trpc-index.ts +0 -215
- package/src/validate-features-runner.ts +0 -107
- package/src/validation-engine.ts +0 -358
- package/src/watch/daemon.ts +0 -385
- package/src/watch/lockfile-detector.ts +0 -65
- package/src/watch/paths.ts +0 -279
- package/src/watch/state.ts +0 -178
package/src/lib/fileLock.ts
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Generic synchronous file-lock primitive built on `proper-lockfile`.
|
|
6
|
-
*
|
|
7
|
-
* Plan 3c gap-59 deliverable. Single source of truth for "acquire-lock,
|
|
8
|
-
* run fn, release on every exit path" across the codebase. Both
|
|
9
|
-
* `lib/installLock.ts:withInstallLock` (Plan 3a installAll serialization)
|
|
10
|
-
* AND `security/manifest-cache.ts:refreshManifest` (Plan 3c manifest
|
|
11
|
-
* cache writes) MUST delegate to `withFileLockSync` here — there is NO
|
|
12
|
-
* parallel lock implementation in the codebase per CR-46 / Rule 0
|
|
13
|
-
* (single-source-of-truth for lock semantics).
|
|
14
|
-
*
|
|
15
|
-
* What this primitive provides:
|
|
16
|
-
* 1. mkdirSync the lock parent dir if absent (fresh-repo / fresh-home case).
|
|
17
|
-
* 2. proper-lockfile.lockSync acquires the lock; we wrap the manual retry
|
|
18
|
-
* loop because lockSync rejects retries>0 (`Cannot use retries with
|
|
19
|
-
* the sync api` per node_modules/proper-lockfile/lib/adapter.js).
|
|
20
|
-
* 3. Surface ELOCKED (POSIX) and EBUSY (Windows) as the same FileLockBusyError.
|
|
21
|
-
* 4. Persist the lock-holder PID alongside the lock as `<lockPath>.pid` so
|
|
22
|
-
* the next contender can include it in a user-friendly error message.
|
|
23
|
-
* 5. Default 30s block-then-bail per Plan 3a §190; configurable per-callsite.
|
|
24
|
-
* 6. `errorFactory` opt lets callers customize the busy-error class so
|
|
25
|
-
* domain-specific helpers (`InstallLockBusyError`, future Phase 5
|
|
26
|
-
* `ManifestCacheBusyError`) can extend the base type without each
|
|
27
|
-
* re-implementing the lock logic.
|
|
28
|
-
*
|
|
29
|
-
* NOT provided by this primitive:
|
|
30
|
-
* - Async variant (`withFileLockAsync`). The current Phase 5 cache-write
|
|
31
|
-
* path resolves the async fetch BEFORE acquiring the lock, so the lock
|
|
32
|
-
* is held only during the brief sync atomicWrite. Async-while-holding-
|
|
33
|
-
* the-lock would deadlock under contention; the design is "fetch first,
|
|
34
|
-
* then lock-for-write only".
|
|
35
|
-
* - Reentrancy. `proper-lockfile.lockSync` is non-reentrant; calling
|
|
36
|
-
* withFileLockSync recursively from inside its own `fn` will fail with
|
|
37
|
-
* ELOCKED. Plan 3a observed and documented this in
|
|
38
|
-
* __tests__/watch/config-refresh-autoyes.test.ts:129.
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
42
|
-
import { dirname } from 'path';
|
|
43
|
-
import * as lockfile from 'proper-lockfile';
|
|
44
|
-
|
|
45
|
-
export interface FileLockOpts {
|
|
46
|
-
/** Default 30s — proper-lockfile considers a lock stale after this elapses. */
|
|
47
|
-
staleMs?: number;
|
|
48
|
-
/**
|
|
49
|
-
* How long the manual retry loop should block waiting for the holder to
|
|
50
|
-
* release before bailing with the busy error. Default 30s.
|
|
51
|
-
* Pass `0` to bail immediately (used in tests).
|
|
52
|
-
*/
|
|
53
|
-
blockMs?: number;
|
|
54
|
-
/** Sleep granularity inside the retry loop. Default 100ms. */
|
|
55
|
-
pollIntervalMs?: number;
|
|
56
|
-
/**
|
|
57
|
-
* Backwards-compat: legacy callers pass `retries: 0` to mean "do not
|
|
58
|
-
* block". When set to a positive integer, used by tests that want to
|
|
59
|
-
* exercise a specific retry count instead of the default time-based loop.
|
|
60
|
-
*/
|
|
61
|
-
retries?: number;
|
|
62
|
-
/** Override clock (test seam). */
|
|
63
|
-
now?: () => number;
|
|
64
|
-
/** Override sleep (test seam). Defaults to a busy-wait spinloop. */
|
|
65
|
-
sleep?: (ms: number) => void;
|
|
66
|
-
/**
|
|
67
|
-
* Optional custom busy-error factory. When provided, the default
|
|
68
|
-
* FileLockBusyError throw is replaced with whatever this factory returns.
|
|
69
|
-
* Domain-specific callers (installLock, manifest-cache) use this to
|
|
70
|
-
* keep their own user-facing error types and messages.
|
|
71
|
-
*/
|
|
72
|
-
errorFactory?: (
|
|
73
|
-
lockPath: string,
|
|
74
|
-
holderPid: number | null,
|
|
75
|
-
retryAfterSeconds: number,
|
|
76
|
-
causeCode: string | undefined,
|
|
77
|
-
) => Error;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export class FileLockBusyError extends Error {
|
|
81
|
-
constructor(
|
|
82
|
-
public lockPath: string,
|
|
83
|
-
public holderPid: number | null,
|
|
84
|
-
public retryAfterSeconds: number,
|
|
85
|
-
public causeCode?: string,
|
|
86
|
-
) {
|
|
87
|
-
const pidPart = holderPid != null ? `(PID=${holderPid})` : '(PID=unknown)';
|
|
88
|
-
super(`File lock at ${lockPath} held by another process ${pidPart} — try again in ${retryAfterSeconds}s`);
|
|
89
|
-
this.name = 'FileLockBusyError';
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Best-effort: read the PID of the current lock holder from the
|
|
95
|
-
* `<lockPath>.pid` sidecar file. Returns null on any read error.
|
|
96
|
-
*/
|
|
97
|
-
export function readLockHolderPid(lockPath: string): number | null {
|
|
98
|
-
try {
|
|
99
|
-
const raw = readFileSync(`${lockPath}.pid`, 'utf-8').trim();
|
|
100
|
-
const pid = Number.parseInt(raw, 10);
|
|
101
|
-
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
102
|
-
return pid;
|
|
103
|
-
} catch {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* CR-9 audit L3 fix: REQUIRES SharedArrayBuffer + Atomics for the sync
|
|
110
|
-
* wait. The prior fallback (a tight `while (Date.now() < end)` spinloop)
|
|
111
|
-
* burned 100% CPU on environments without Atomics — typically sandboxed
|
|
112
|
-
* serverless runtimes — making contended manifest refreshes a DoS
|
|
113
|
-
* surface against the host. With this throw, callers running on
|
|
114
|
-
* Atomics-less environments fail loudly + early instead of silently
|
|
115
|
-
* spinning.
|
|
116
|
-
*/
|
|
117
|
-
export function busyWaitSync(ms: number): void {
|
|
118
|
-
if (typeof SharedArrayBuffer === 'undefined' || typeof Atomics === 'undefined') {
|
|
119
|
-
throw new Error(
|
|
120
|
-
`withFileLockSync requires SharedArrayBuffer + Atomics for its retry-loop wait. ` +
|
|
121
|
-
`This Node runtime does not provide them — refusing to fall back to a CPU spinloop. ` +
|
|
122
|
-
`If you hit this in a sandboxed serverless env, the fix is to perform the ` +
|
|
123
|
-
`lock-protected operation in a host runtime that supports Atomics.`,
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
const sab = new SharedArrayBuffer(4);
|
|
127
|
-
const view = new Int32Array(sab);
|
|
128
|
-
Atomics.wait(view, 0, 0, ms);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Acquire the lock at `lockPath`, run `fn`, release on every exit path.
|
|
133
|
-
* Synchronous all the way through. See module-level doc for guarantees.
|
|
134
|
-
*
|
|
135
|
-
* Throws:
|
|
136
|
-
* - The result of `opts.errorFactory(...)` if provided AND lock is busy
|
|
137
|
-
* beyond `blockMs`. Otherwise throws FileLockBusyError.
|
|
138
|
-
* - Any non-ELOCKED/EBUSY filesystem error from proper-lockfile is
|
|
139
|
-
* re-thrown unchanged.
|
|
140
|
-
*/
|
|
141
|
-
export function withFileLockSync<T>(lockPath: string, fn: () => T, opts: FileLockOpts = {}): T {
|
|
142
|
-
// Ensure the lock's parent directory exists. Fresh repos / fresh user-home
|
|
143
|
-
// .massu/ may not have the parent yet.
|
|
144
|
-
mkdirSync(dirname(lockPath), { recursive: true });
|
|
145
|
-
|
|
146
|
-
const staleMs = opts.staleMs ?? 30_000;
|
|
147
|
-
const blockMs = opts.retries === 0 ? 0 : (opts.blockMs ?? 30_000);
|
|
148
|
-
const pollIntervalMs = opts.pollIntervalMs ?? 100;
|
|
149
|
-
const now = opts.now ?? Date.now;
|
|
150
|
-
const sleep = opts.sleep ?? busyWaitSync;
|
|
151
|
-
const makeBusyError =
|
|
152
|
-
opts.errorFactory ??
|
|
153
|
-
((path, pid, retrySeconds, code) => new FileLockBusyError(path, pid, retrySeconds, code));
|
|
154
|
-
|
|
155
|
-
let release: (() => void) | null = null;
|
|
156
|
-
const deadline = now() + blockMs;
|
|
157
|
-
|
|
158
|
-
for (;;) {
|
|
159
|
-
try {
|
|
160
|
-
release = lockfile.lockSync(lockPath, {
|
|
161
|
-
stale: staleMs,
|
|
162
|
-
retries: 0,
|
|
163
|
-
realpath: false,
|
|
164
|
-
});
|
|
165
|
-
try {
|
|
166
|
-
writeFileSync(`${lockPath}.pid`, String(process.pid), 'utf-8');
|
|
167
|
-
} catch {
|
|
168
|
-
// best-effort
|
|
169
|
-
}
|
|
170
|
-
break;
|
|
171
|
-
} catch (err) {
|
|
172
|
-
const e = err as NodeJS.ErrnoException;
|
|
173
|
-
const code = e.code;
|
|
174
|
-
if (code !== 'ELOCKED' && code !== 'EBUSY') {
|
|
175
|
-
throw err;
|
|
176
|
-
}
|
|
177
|
-
if (now() >= deadline) {
|
|
178
|
-
const holderPid = readLockHolderPid(lockPath);
|
|
179
|
-
const remainingMs = Math.max(0, deadline - now());
|
|
180
|
-
const retryAfterSeconds = blockMs === 0
|
|
181
|
-
? Math.round(staleMs / 1000)
|
|
182
|
-
: Math.round(remainingMs / 1000);
|
|
183
|
-
throw makeBusyError(lockPath, holderPid, retryAfterSeconds, code);
|
|
184
|
-
}
|
|
185
|
-
sleep(pollIntervalMs);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
return fn();
|
|
191
|
-
} finally {
|
|
192
|
-
try {
|
|
193
|
-
if (release) release();
|
|
194
|
-
} catch {
|
|
195
|
-
// best-effort
|
|
196
|
-
}
|
|
197
|
-
try {
|
|
198
|
-
rmSync(`${lockPath}.pid`, { force: true });
|
|
199
|
-
} catch {
|
|
200
|
-
// best-effort
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
package/src/lib/gitToplevel.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Resolve the git repository root for a given working directory, falling
|
|
6
|
-
* back to the cwd itself when not inside a git repo.
|
|
7
|
-
*
|
|
8
|
-
* Used by `massu watch` and `massu refresh-log` so the watcher root and the
|
|
9
|
-
* refresh-log path always anchor on the same toplevel rather than wherever
|
|
10
|
-
* the user happened to invoke the CLI from.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { spawnSync } from 'child_process';
|
|
14
|
-
|
|
15
|
-
export function gitToplevel(cwd: string): string {
|
|
16
|
-
const res = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
17
|
-
cwd,
|
|
18
|
-
encoding: 'utf-8',
|
|
19
|
-
});
|
|
20
|
-
if (res.status === 0 && res.stdout) return res.stdout.trim();
|
|
21
|
-
return cwd;
|
|
22
|
-
}
|
package/src/lib/hook-registry.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Single source of truth for the canonical Massu hook set.
|
|
3
|
-
*
|
|
4
|
-
* P-H001 (plan-stage-c-high-batch): doctor / installer / build:hooks all
|
|
5
|
-
* consume from here. The drift-guard `hook-registry-parity.test.ts` asserts:
|
|
6
|
-
*
|
|
7
|
-
* 1. REGISTERED_HOOKS matches `src/hooks/*.ts` filenames (the build SoT).
|
|
8
|
-
* 2. REGISTERED_HOOKS matches the hook names referenced in
|
|
9
|
-
* `buildHooksConfig()` (the installer SoT).
|
|
10
|
-
* 3. REGISTERED_HOOKS matches `dist/hooks/*.js` after `npm run build:hooks`
|
|
11
|
-
* (the runtime SoT, when build artifacts are available).
|
|
12
|
-
*
|
|
13
|
-
* Adding a new hook requires touching THREE places: src/hooks/X.ts,
|
|
14
|
-
* REGISTERED_HOOKS, and buildHooksConfig() — the parity tests enforce this.
|
|
15
|
-
*/
|
|
16
|
-
export const REGISTERED_HOOKS = [
|
|
17
|
-
'auto-learning-pipeline',
|
|
18
|
-
'classify-failure',
|
|
19
|
-
'cost-tracker',
|
|
20
|
-
'fix-detector',
|
|
21
|
-
'incident-pipeline',
|
|
22
|
-
'intent-suggester',
|
|
23
|
-
'post-edit-context',
|
|
24
|
-
'post-tool-use',
|
|
25
|
-
'pre-compact',
|
|
26
|
-
'pre-delete-check',
|
|
27
|
-
'quality-event',
|
|
28
|
-
'rule-enforcement-pipeline',
|
|
29
|
-
'security-gate',
|
|
30
|
-
'session-end',
|
|
31
|
-
'session-start',
|
|
32
|
-
'user-prompt',
|
|
33
|
-
] as const;
|
|
34
|
-
|
|
35
|
-
export type RegisteredHook = (typeof REGISTERED_HOOKS)[number];
|
|
36
|
-
|
|
37
|
-
export function getExpectedHookFiles(): readonly string[] {
|
|
38
|
-
return REGISTERED_HOOKS.map((name) => `${name}.js`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function getRegisteredHookCount(): number {
|
|
42
|
-
return REGISTERED_HOOKS.length;
|
|
43
|
-
}
|
package/src/lib/installLock.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Synchronous file lock around installAll() for cross-process safety.
|
|
6
|
-
*
|
|
7
|
-
* Plan 3a Phase 6: installAll() may now be invoked from BOTH the manual
|
|
8
|
-
* `runConfigRefresh` path AND the watcher auto-trigger. Without
|
|
9
|
-
* serialization, two concurrent callers can race on `.claude/commands/`
|
|
10
|
-
* file writes. proper-lockfile gives us atomic mkdir-based locks that
|
|
11
|
-
* work cross-platform.
|
|
12
|
-
*
|
|
13
|
-
* Plan 3c gap-59 / Rule 0 single-source-of-truth refactor (commit pending,
|
|
14
|
-
* 2026-05-07): the proper-lockfile-wrapping logic + manual retry loop +
|
|
15
|
-
* .pid sidecar bookkeeping now lives in `lib/fileLock.ts:withFileLockSync`.
|
|
16
|
-
* This file is a thin domain-specific wrapper that:
|
|
17
|
-
* 1. Computes the project-root-anchored lockPath
|
|
18
|
-
* 2. Delegates to withFileLockSync via an `errorFactory` that returns
|
|
19
|
-
* `InstallLockBusyError` (preserving the exact error message format
|
|
20
|
-
* Plan 3a §243 specified and the install-lock tests assert)
|
|
21
|
-
*
|
|
22
|
-
* Plan 3a §190 retry behavior preserved: "second caller blocks up to 30s,
|
|
23
|
-
* then bails". InstallLockBusyError instances continue to expose
|
|
24
|
-
* `lockPath`, `holderPid`, `retryAfterSeconds`, `causeCode` — backwards-
|
|
25
|
-
* compatible for any caller that does `instanceof InstallLockBusyError`.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import { resolve } from 'path';
|
|
29
|
-
import { withFileLockSync, type FileLockOpts } from './fileLock.js';
|
|
30
|
-
|
|
31
|
-
export interface InstallLockOpts extends FileLockOpts {
|
|
32
|
-
// No additional fields — all options come from FileLockOpts. This alias
|
|
33
|
-
// preserves the public surface for any caller that imported `InstallLockOpts`.
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export class InstallLockBusyError extends Error {
|
|
37
|
-
constructor(
|
|
38
|
-
public lockPath: string,
|
|
39
|
-
public holderPid: number | null,
|
|
40
|
-
public retryAfterSeconds: number,
|
|
41
|
-
public causeCode?: string,
|
|
42
|
-
) {
|
|
43
|
-
const pidPart = holderPid != null ? `(PID=${holderPid})` : '(PID=unknown)';
|
|
44
|
-
super(`installAll already running ${pidPart} — try again in ${retryAfterSeconds}s`);
|
|
45
|
-
this.name = 'InstallLockBusyError';
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Acquire the install lock for `projectRoot`, run `fn`, release on every
|
|
51
|
-
* exit path. Throws `InstallLockBusyError` when the lock is held beyond
|
|
52
|
-
* `blockMs`. See `lib/fileLock.ts:withFileLockSync` for the underlying
|
|
53
|
-
* primitive.
|
|
54
|
-
*/
|
|
55
|
-
export function withInstallLock<T>(projectRoot: string, fn: () => T, opts: InstallLockOpts = {}): T {
|
|
56
|
-
const lockPath = resolve(projectRoot, '.massu', 'installAll.lock');
|
|
57
|
-
return withFileLockSync(
|
|
58
|
-
lockPath,
|
|
59
|
-
fn,
|
|
60
|
-
{
|
|
61
|
-
...opts,
|
|
62
|
-
errorFactory: (path, pid, retrySeconds, code) =>
|
|
63
|
-
new InstallLockBusyError(path, pid, retrySeconds, code),
|
|
64
|
-
},
|
|
65
|
-
);
|
|
66
|
-
}
|
package/src/lib/memory-path.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Memory directory name encoding — canonical helpers.
|
|
6
|
-
*
|
|
7
|
-
* Single source of truth for translating a project's absolute filesystem
|
|
8
|
-
* root into the directory name used under `~/.claude/projects/<encoded-root>/memory/`.
|
|
9
|
-
*
|
|
10
|
-
* Closes the P-004 install-path drift class: the writer in `commands/init.ts`
|
|
11
|
-
* historically prepended an EXTRA leading `-` while the reader in
|
|
12
|
-
* `config.ts:getResolvedPaths()` and the backfill code path used the
|
|
13
|
-
* canonical single-dash form. Result: 100% of `massu init` runs orphaned
|
|
14
|
-
* `MEMORY.md` in a directory the reader could never find.
|
|
15
|
-
*
|
|
16
|
-
* Both encoding and decoding live here so the round-trip property is testable.
|
|
17
|
-
*
|
|
18
|
-
* Encoding rule:
|
|
19
|
-
* Replace every `/` in the absolute project root with `-`.
|
|
20
|
-
* An absolute path always starts with `/`, so the result always starts with
|
|
21
|
-
* `-` exactly once. NEVER prepend an additional `-`.
|
|
22
|
-
*
|
|
23
|
-
* Decoding rule:
|
|
24
|
-
* Replace every `-` in the directory name with `/`. This is the canonical
|
|
25
|
-
* inverse used by Claude Code's session-state plumbing. Note: project roots
|
|
26
|
-
* that contain literal `-` characters cannot be unambiguously round-tripped
|
|
27
|
-
* through this encoding — the same trade-off Claude Code's own resolver makes.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Encode an absolute project root into the directory name used under
|
|
32
|
-
* `~/.claude/projects/<dir>/memory/`.
|
|
33
|
-
*
|
|
34
|
-
* @param projectRoot Absolute filesystem path (must start with `/`).
|
|
35
|
-
* @returns Canonical encoded directory name (always begins with `-`).
|
|
36
|
-
*/
|
|
37
|
-
export function encodeMemoryDirName(projectRoot: string): string {
|
|
38
|
-
return projectRoot.replace(/\//g, '-');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Decode a memory-directory name back to its slash-separated form.
|
|
43
|
-
*
|
|
44
|
-
* @param dirname The directory name as it appears under `~/.claude/projects/`.
|
|
45
|
-
* @returns A slash-separated path (begins with `/`).
|
|
46
|
-
*/
|
|
47
|
-
export function decodeMemoryDirName(dirname: string): string {
|
|
48
|
-
return dirname.replace(/-/g, '/');
|
|
49
|
-
}
|
package/src/lib/pidLiveness.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Cross-platform PID liveness probe.
|
|
6
|
-
*
|
|
7
|
-
* POSIX (macOS / Linux):
|
|
8
|
-
* process.kill(pid, 0) → no-throw == alive; ESRCH → dead; EPERM → alive
|
|
9
|
-
* (the process exists but we lack permission to signal it).
|
|
10
|
-
*
|
|
11
|
-
* Windows:
|
|
12
|
-
* `tasklist /FI "PID eq <pid>" /NH` and grep for the PID. Best-effort.
|
|
13
|
-
*
|
|
14
|
-
* Returns boolean; never throws. Used by hooks/session-start.ts
|
|
15
|
-
* (banner suppression) and watch/daemon.ts (registry sweep).
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { spawnSync } from 'child_process';
|
|
19
|
-
|
|
20
|
-
interface ProbeOpts {
|
|
21
|
-
/** When set, override `process.platform` (test seam). */
|
|
22
|
-
platformOverride?: NodeJS.Platform;
|
|
23
|
-
/** When set, override `process.kill` (test seam). */
|
|
24
|
-
killOverride?: (pid: number, signal: number) => boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function isPidAlive(pid: number, opts: ProbeOpts = {}): boolean {
|
|
28
|
-
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
29
|
-
|
|
30
|
-
const platform = opts.platformOverride ?? process.platform;
|
|
31
|
-
|
|
32
|
-
if (platform === 'win32') {
|
|
33
|
-
return checkWindows(pid);
|
|
34
|
-
}
|
|
35
|
-
return checkPosix(pid, opts.killOverride);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function checkPosix(pid: number, killOverride?: (pid: number, signal: number) => boolean): boolean {
|
|
39
|
-
try {
|
|
40
|
-
if (killOverride) {
|
|
41
|
-
killOverride(pid, 0);
|
|
42
|
-
} else {
|
|
43
|
-
process.kill(pid, 0);
|
|
44
|
-
}
|
|
45
|
-
return true;
|
|
46
|
-
} catch (err) {
|
|
47
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
48
|
-
if (code === 'EPERM') return true;
|
|
49
|
-
// ESRCH or anything else → treat as dead.
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function checkWindows(pid: number): boolean {
|
|
55
|
-
try {
|
|
56
|
-
const res = spawnSync('tasklist', ['/FI', `PID eq ${pid}`, '/NH'], {
|
|
57
|
-
encoding: 'utf-8',
|
|
58
|
-
windowsHide: true,
|
|
59
|
-
});
|
|
60
|
-
if (res.error || res.status !== 0) return false;
|
|
61
|
-
const stdout = res.stdout || '';
|
|
62
|
-
// Each match is a line that starts with the image name and includes the PID.
|
|
63
|
-
return new RegExp(`\\b${pid}\\b`).test(stdout);
|
|
64
|
-
} catch {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Shared IO helper for `.claude/settings.local.json` and the user-global
|
|
6
|
-
* `~/.claude/settings.json`. SSOT for read+atomic-write semantics; closes
|
|
7
|
-
* the pre-existing non-atomic-write bug at init.ts installHooks (previously
|
|
8
|
-
* used writeFileSync which was vulnerable to SIGINT-between-truncate-and-write).
|
|
9
|
-
*
|
|
10
|
-
* Three exports:
|
|
11
|
-
* - readSettingsLocal(claudeDir): safe-parse of <claudeDir>/settings.local.json
|
|
12
|
-
* - writeSettingsLocalAtomic(claudeDir, settings): atomic write of same
|
|
13
|
-
* - readSettingsAtPath(absolutePath): generic safe-parse used by readSettingsLocal
|
|
14
|
-
* AND by permissions.ts:readGlobalSettings() reading ~/.claude/settings.json
|
|
15
|
-
*
|
|
16
|
-
* Plus the atomicWriteFile primitive (moved here from install-commands.ts to
|
|
17
|
-
* centralize). All consumers — install-commands manifest save, install-commands
|
|
18
|
-
* per-file syncs, init.ts installHooks, doctor.ts hooks-config check — share
|
|
19
|
-
* this single source of truth for filesystem IO.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
closeSync,
|
|
24
|
-
existsSync,
|
|
25
|
-
fsyncSync,
|
|
26
|
-
mkdirSync,
|
|
27
|
-
openSync,
|
|
28
|
-
readFileSync,
|
|
29
|
-
renameSync,
|
|
30
|
-
rmSync,
|
|
31
|
-
writeSync,
|
|
32
|
-
} from 'fs';
|
|
33
|
-
import { dirname, resolve } from 'path';
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Atomic file write — tmp + fsync + rename. Moved from install-commands.ts so
|
|
37
|
-
* it can be reused by settings-local IO without circular import.
|
|
38
|
-
*
|
|
39
|
-
* Writes via openSync + writeSync + fsyncSync + closeSync + renameSync so the
|
|
40
|
-
* data hits the platter before the rename. On any error, removes the tmp file.
|
|
41
|
-
* Tmp filename includes process.pid to avoid clashes with concurrent installs.
|
|
42
|
-
*/
|
|
43
|
-
export function atomicWriteFile(targetPath: string, content: string, mode = 0o644): void {
|
|
44
|
-
const tmpPath = `${targetPath}.${process.pid}.tmp`;
|
|
45
|
-
try {
|
|
46
|
-
const fd = openSync(tmpPath, 'w', mode);
|
|
47
|
-
try {
|
|
48
|
-
const buf = Buffer.from(content, 'utf-8');
|
|
49
|
-
writeSync(fd, buf, 0, buf.length, 0);
|
|
50
|
-
fsyncSync(fd);
|
|
51
|
-
} finally {
|
|
52
|
-
closeSync(fd);
|
|
53
|
-
}
|
|
54
|
-
renameSync(tmpPath, targetPath);
|
|
55
|
-
} catch (err) {
|
|
56
|
-
if (existsSync(tmpPath)) {
|
|
57
|
-
try { rmSync(tmpPath, { force: true }); } catch { /* ignore */ }
|
|
58
|
-
}
|
|
59
|
-
throw err;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Generic safe-parse of a JSON settings file. Used by readSettingsLocal
|
|
65
|
-
* (project-local) AND permissions.ts:readGlobalSettings (user-global).
|
|
66
|
-
*
|
|
67
|
-
* Returns `{}` on any failure path: missing file, unreadable, malformed JSON,
|
|
68
|
-
* non-object root. This contract lets callers do `(settings.permissions as any)`
|
|
69
|
-
* shape-checks defensively without a separate "does the file exist" pre-check.
|
|
70
|
-
*/
|
|
71
|
-
export function readSettingsAtPath(absolutePath: string): Record<string, unknown> {
|
|
72
|
-
if (!existsSync(absolutePath)) {
|
|
73
|
-
return {};
|
|
74
|
-
}
|
|
75
|
-
try {
|
|
76
|
-
const raw = readFileSync(absolutePath, 'utf-8');
|
|
77
|
-
const parsed = JSON.parse(raw);
|
|
78
|
-
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
79
|
-
return {};
|
|
80
|
-
}
|
|
81
|
-
return parsed as Record<string, unknown>;
|
|
82
|
-
} catch {
|
|
83
|
-
return {};
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Read `<claudeDir>/settings.local.json`. Returns `{}` on missing/corrupt.
|
|
89
|
-
*/
|
|
90
|
-
export function readSettingsLocal(claudeDir: string): Record<string, unknown> {
|
|
91
|
-
return readSettingsAtPath(resolve(claudeDir, 'settings.local.json'));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Atomically write `<claudeDir>/settings.local.json`. Creates the parent
|
|
96
|
-
* directory if missing. JSON.stringify with 2-space indent + trailing newline
|
|
97
|
-
* (matches the existing convention in init.ts installHooks).
|
|
98
|
-
*/
|
|
99
|
-
export function writeSettingsLocalAtomic(
|
|
100
|
-
claudeDir: string,
|
|
101
|
-
settings: Record<string, unknown>,
|
|
102
|
-
): void {
|
|
103
|
-
const targetPath = resolve(claudeDir, 'settings.local.json');
|
|
104
|
-
const dir = dirname(targetPath);
|
|
105
|
-
if (!existsSync(dir)) {
|
|
106
|
-
mkdirSync(dir, { recursive: true });
|
|
107
|
-
}
|
|
108
|
-
const content = JSON.stringify(settings, null, 2) + '\n';
|
|
109
|
-
atomicWriteFile(targetPath, content);
|
|
110
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* P-H032 (plan-stage-c-high-batch / 1.10.7): SoT for SQL table names that
|
|
6
|
-
* are prefixed with the customer's configured `toolPrefix`.
|
|
7
|
-
*
|
|
8
|
-
* **Bug class closed**: 20 distinct SQL table names (`massu_imports`,
|
|
9
|
-
* `massu_trpc_procedures`, `massu_sentinel`, etc.) were hardcoded with the
|
|
10
|
-
* `massu_` prefix in ~145 SQL string sites across 21 source files. Tool
|
|
11
|
-
* names were already config-driven via `getConfig().toolPrefix` but
|
|
12
|
-
* persistence wasn't — split-brain where a non-default-prefix customer
|
|
13
|
-
* (e.g. `toolPrefix: 'foo'`) would have tools named `foo_*` but tables
|
|
14
|
-
* still named `massu_*`. Default-prefix customers (100% of current
|
|
15
|
-
* installs) see ZERO behavior change.
|
|
16
|
-
*
|
|
17
|
-
* **Structural fix**: every SQL string that references a prefix-scoped
|
|
18
|
-
* table now goes through `t(suffix)` which returns
|
|
19
|
-
* `\`${getConfig().toolPrefix}_${suffix}\``. The ESLint rule
|
|
20
|
-
* `no-hardcoded-massu-tables` bans bare `'massu_X'` literals so future
|
|
21
|
-
* regressions are impossible. The drift-guard test
|
|
22
|
-
* `sql-table-names-source-grep.test.ts` scans the source tree and asserts
|
|
23
|
-
* zero bare `massu_<table>` literals outside this module + the test files.
|
|
24
|
-
*
|
|
25
|
-
* **20 canonical table suffixes** (registered here as a typed const so
|
|
26
|
-
* typos at callsites are caught at compile time):
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import { getConfig } from '../config.ts';
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* The complete set of suffixes for prefix-scoped SQL tables. Keep this in
|
|
33
|
-
* sync with `packages/core/src/db.ts` (CodeGraph schema) +
|
|
34
|
-
* `sentinel-db.ts` (sentinel schema) + Python indexer DDL in
|
|
35
|
-
* `packages/core/src/python/*.ts`.
|
|
36
|
-
*/
|
|
37
|
-
export type MassuTableSuffix =
|
|
38
|
-
| 'imports'
|
|
39
|
-
| 'meta'
|
|
40
|
-
| 'middleware_tree'
|
|
41
|
-
| 'page_deps'
|
|
42
|
-
| 'py_fk_edges'
|
|
43
|
-
| 'py_imports'
|
|
44
|
-
| 'py_meta'
|
|
45
|
-
| 'py_migrations'
|
|
46
|
-
| 'py_models'
|
|
47
|
-
| 'py_route_callers'
|
|
48
|
-
| 'py_routes'
|
|
49
|
-
| 'sentinel'
|
|
50
|
-
| 'sentinel_changelog'
|
|
51
|
-
| 'sentinel_components'
|
|
52
|
-
| 'sentinel_deps'
|
|
53
|
-
| 'sentinel_fts'
|
|
54
|
-
| 'sentinel_pages'
|
|
55
|
-
| 'sentinel_procedures'
|
|
56
|
-
| 'trpc_call_sites'
|
|
57
|
-
| 'trpc_procedures';
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Resolve a prefix-scoped table name to its concrete identifier using the
|
|
61
|
-
* current `getConfig().toolPrefix`. For default-prefix installs returns
|
|
62
|
-
* `massu_<suffix>` (current behavior). For custom-prefix installs returns
|
|
63
|
-
* `<customPrefix>_<suffix>`.
|
|
64
|
-
*
|
|
65
|
-
* Use this in EVERY SQL string that references a prefix-scoped table.
|
|
66
|
-
* The ESLint rule bans bare `'massu_X'` literals; the only legitimate
|
|
67
|
-
* exit is this function.
|
|
68
|
-
*/
|
|
69
|
-
export function t(suffix: MassuTableSuffix): string {
|
|
70
|
-
return `${getConfig().toolPrefix}_${suffix}`;
|
|
71
|
-
}
|