@massu/core 1.4.0 → 1.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/dist/cli.js +9445 -5483
- package/dist/hooks/auto-learning-pipeline.js +18 -0
- package/dist/hooks/classify-failure.js +18 -0
- package/dist/hooks/cost-tracker.js +18 -0
- package/dist/hooks/fix-detector.js +18 -0
- package/dist/hooks/incident-pipeline.js +18 -0
- package/dist/hooks/post-edit-context.js +18 -0
- package/dist/hooks/post-tool-use.js +18 -0
- package/dist/hooks/pre-compact.js +18 -0
- package/dist/hooks/pre-delete-check.js +18 -0
- package/dist/hooks/quality-event.js +18 -0
- package/dist/hooks/rule-enforcement-pipeline.js +18 -0
- package/dist/hooks/session-end.js +18 -0
- package/dist/hooks/session-start.js +2668 -2674
- package/dist/hooks/user-prompt.js +18 -0
- package/docs/AUTHORING-ADAPTERS.md +207 -0
- package/docs/SECURITY.md +250 -0
- package/package.json +7 -3
- package/src/adapter.ts +90 -0
- package/src/cli.ts +7 -0
- package/src/commands/adapters.ts +824 -0
- package/src/commands/config-check-drift.ts +1 -0
- package/src/commands/config-refresh.ts +1 -0
- package/src/commands/config-upgrade.ts +1 -0
- package/src/commands/doctor.ts +2 -0
- package/src/commands/init.ts +2 -0
- package/src/config.ts +63 -0
- package/src/detect/adapters/aspnet.ts +293 -0
- package/src/detect/adapters/discover.ts +469 -0
- package/src/detect/adapters/go-chi.ts +261 -0
- package/src/detect/adapters/index.ts +49 -0
- package/src/detect/adapters/phoenix.ts +277 -0
- package/src/detect/adapters/python-flask.ts +235 -0
- package/src/detect/adapters/rails.ts +279 -0
- package/src/detect/adapters/runner.ts +32 -0
- package/src/detect/adapters/spring.ts +284 -0
- package/src/detect/adapters/tree-sitter-loader.ts +50 -0
- package/src/detect/adapters/types.ts +18 -0
- package/src/detect/monorepo-detector.ts +1 -0
- package/src/hooks/post-tool-use.ts +1 -0
- package/src/hooks/session-start.ts +1 -0
- package/src/lib/fileLock.ts +203 -0
- package/src/lib/installLock.ts +31 -144
- package/src/memory-file-ingest.ts +1 -0
- package/src/security/adapter-origin.ts +130 -0
- package/src/security/adapter-verifier.ts +319 -0
- package/src/security/atomic-write.ts +164 -0
- package/src/security/fetcher.ts +200 -0
- package/src/security/install-tracking.ts +319 -0
- package/src/security/local-fingerprint.ts +225 -0
- package/src/security/manifest-cache.ts +333 -0
- package/src/security/manifest-schema.ts +129 -0
- package/src/security/registry-pubkey.generated.ts +35 -0
- package/src/security/telemetry.ts +320 -0
- package/templates/aspnet/massu.config.yaml +57 -0
- package/templates/go-chi/massu.config.yaml +52 -0
- package/templates/phoenix/massu.config.yaml +54 -0
- package/templates/python-flask/massu.config.yaml +51 -0
- package/templates/rails/massu.config.yaml +56 -0
- package/templates/spring/massu.config.yaml +56 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-08T15:19:53.775Z.
|
|
2
|
+
// Source pem: packages/core/security/registry-pubkey.pem
|
|
3
|
+
// RAW-bytes sha256: 3b6226d036c472e533110d11a7d0cd2773ce1d7d4f1003517d5bd69c5418ed4c
|
|
4
|
+
// DO NOT EDIT — regenerate via `node scripts/bundle-pubkey.mjs` or
|
|
5
|
+
// the prepublishOnly hook (which runs on every npm publish).
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Ed25519 public key for the Massu adapter registry (registry.massu.ai).
|
|
9
|
+
* Used by packages/core/src/security/adapter-verifier.ts to verify signed
|
|
10
|
+
* manifests. Plan 3c gap-29 (Phase D P4-003) generated and vendored.
|
|
11
|
+
*
|
|
12
|
+
* To rotate: regenerate the keypair (operator-only, store private in
|
|
13
|
+
* macOS Keychain at `massu/registry/signing/private`), update
|
|
14
|
+
* packages/core/security/registry-pubkey.pem with the new public key,
|
|
15
|
+
* append the new RAW-bytes sha256 to KNOWN_PUBKEY_FINGERPRINTS in
|
|
16
|
+
* scripts/bundle-pubkey.mjs (DO NOT delete the old entry — rotation grace
|
|
17
|
+
* window per gap-54), then run `node scripts/bundle-pubkey.mjs`.
|
|
18
|
+
*/
|
|
19
|
+
export const REGISTRY_PUBKEY_ED25519: Uint8Array = new Uint8Array([12, 36, 232, 78, 159, 80, 198, 224, 21, 238, 144, 34, 84, 236, 219, 135, 224, 122, 164, 204, 0, 204, 155, 131, 30, 57, 213, 182, 61, 243, 141, 7]);
|
|
20
|
+
|
|
21
|
+
/** Hex sha256 of REGISTRY_PUBKEY_ED25519 (raw 32 bytes). */
|
|
22
|
+
export const REGISTRY_PUBKEY_FINGERPRINT_HEX = '3b6226d036c472e533110d11a7d0cd2773ce1d7d4f1003517d5bd69c5418ed4c';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Allowlist of historically-trusted pubkey fingerprints. CR-9 audit L2
|
|
26
|
+
* fix: the runtime verifier asserts that REGISTRY_PUBKEY_FINGERPRINT_HEX
|
|
27
|
+
* is a member of this set; a bundled key that does not appear in the
|
|
28
|
+
* allowlist refuses to load even if compiled in. Mirror of
|
|
29
|
+
* KNOWN_PUBKEY_FINGERPRINTS in scripts/bundle-pubkey.mjs (kept in sync
|
|
30
|
+
* by the bundle script's emit step). Append on rotation; never remove
|
|
31
|
+
* during the grace window.
|
|
32
|
+
*/
|
|
33
|
+
export const KNOWN_PUBKEY_FINGERPRINTS: ReadonlySet<string> = new Set([
|
|
34
|
+
"3b6226d036c472e533110d11a7d0cd2773ce1d7d4f1003517d5bd69c5418ed4c"
|
|
35
|
+
]);
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anonymous adapter-discovery telemetry writer + replay (Plan 3c gap-22 /
|
|
3
|
+
* VR-TELEMETRY-PAYLOAD-SCHEMA). STRICTLY off by default. Never sends anything
|
|
4
|
+
* unless `massu.config.yaml > telemetry.adapters: true` is explicitly set
|
|
5
|
+
* AND the operator has read SECURITY.md.
|
|
6
|
+
*
|
|
7
|
+
* What gets sent (THE ONLY four fields, enforced by `.strict()` Zod schema):
|
|
8
|
+
* - adapter_id: string — the canonical id (e.g. "@massu/adapter-rails", "python-fastapi")
|
|
9
|
+
* - count: integer >= 0 — how many adapter-discovery events were observed in this batch
|
|
10
|
+
* - version: string — the adapter version (when known); empty string for CORE-BUNDLED
|
|
11
|
+
* - ts: ISO8601 string — when the event was recorded
|
|
12
|
+
*
|
|
13
|
+
* What does NOT get sent (PII guardrail):
|
|
14
|
+
* - file paths
|
|
15
|
+
* - symbol names
|
|
16
|
+
* - source code content
|
|
17
|
+
* - project names
|
|
18
|
+
* - operator identity
|
|
19
|
+
*
|
|
20
|
+
* The schema is `.strict()` — unknown keys are REJECTED at write time AND at
|
|
21
|
+
* replay time. A future bug that adds a non-allowlisted field cannot leak data
|
|
22
|
+
* because the writer refuses to record an unknown key, and the replay step
|
|
23
|
+
* re-validates against the same schema (drops stale entries that no longer
|
|
24
|
+
* pass — Plan 3c iter1 cross-cutting check #14).
|
|
25
|
+
*
|
|
26
|
+
* Buffer bounds (Plan 3c iter1 fix #14):
|
|
27
|
+
* - Pending JSONL file caps at 1 MB OR 1000 entries (whichever first)
|
|
28
|
+
* - On overflow, drop OLDEST entries with stderr warning once per startup
|
|
29
|
+
* - Replay backoff: exponential 1s → 60s cap, max 10 attempts per startup
|
|
30
|
+
* - Re-validate every entry at replay against the SAME schema (drop stale)
|
|
31
|
+
*
|
|
32
|
+
* File locations (per Plan 3c gap-37 file-mode discipline):
|
|
33
|
+
* - ~/.massu/telemetry-pending.jsonl (mode 0o600, append-only)
|
|
34
|
+
* - ~/.massu/ (mode 0o700, owner-rwx only)
|
|
35
|
+
*/
|
|
36
|
+
import { existsSync, readFileSync, statSync, chmodSync, appendFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
37
|
+
import { dirname, resolve } from 'node:path';
|
|
38
|
+
import { homedir } from 'node:os';
|
|
39
|
+
import { z } from 'zod';
|
|
40
|
+
import { fetchUrl, FetchAllowlistError, FetchTimeoutError } from './fetcher.js';
|
|
41
|
+
import { isGroupOrWorldWritable } from './atomic-write.js';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* THE schema for adapter-discovery telemetry payloads. `.strict()` rejects
|
|
45
|
+
* unknown keys at parse time — a bug that adds an unlisted field is impossible
|
|
46
|
+
* to ship because both the writer and the replay re-validate against this
|
|
47
|
+
* exact schema.
|
|
48
|
+
*/
|
|
49
|
+
export const AdapterDiscoveryPayloadSchema = z.object({
|
|
50
|
+
adapter_id: z.string().min(1).max(256),
|
|
51
|
+
count: z.number().int().nonnegative(),
|
|
52
|
+
version: z.string().max(64),
|
|
53
|
+
ts: z.string().datetime(),
|
|
54
|
+
}).strict();
|
|
55
|
+
export type AdapterDiscoveryPayload = z.infer<typeof AdapterDiscoveryPayloadSchema>;
|
|
56
|
+
|
|
57
|
+
const TELEMETRY_ENDPOINT = 'https://telemetry.massu.ai/adapter-discovery';
|
|
58
|
+
const PENDING_PATH = resolve(homedir(), '.massu', 'telemetry-pending.jsonl');
|
|
59
|
+
const MAX_BUFFER_BYTES = 1_000_000; // 1 MB
|
|
60
|
+
const MAX_BUFFER_ENTRIES = 1_000;
|
|
61
|
+
const REPLAY_MAX_ATTEMPTS = 10;
|
|
62
|
+
const REPLAY_BACKOFF_MIN_MS = 1_000;
|
|
63
|
+
const REPLAY_BACKOFF_MAX_MS = 60_000;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Result of a single recordAdapterDiscovery call.
|
|
67
|
+
*
|
|
68
|
+
* - 'sent' — payload posted to the live endpoint.
|
|
69
|
+
* - 'queued' — payload appended to ~/.massu/telemetry-pending.jsonl
|
|
70
|
+
* for replay on next startup (endpoint unreachable but
|
|
71
|
+
* payload was schema-valid).
|
|
72
|
+
* - 'dropped' — payload was schema-invalid (unknown keys, wrong types,
|
|
73
|
+
* PII attempt) and was dropped without writing or sending.
|
|
74
|
+
* Caller may surface a stderr warning naming the offending
|
|
75
|
+
* field.
|
|
76
|
+
* - 'disabled' — telemetry.adapters is false; payload was not validated,
|
|
77
|
+
* not written, not sent. This is the default state.
|
|
78
|
+
*/
|
|
79
|
+
export type RecordResult =
|
|
80
|
+
| { kind: 'sent' }
|
|
81
|
+
| { kind: 'queued'; pendingBytes: number; entryCount: number }
|
|
82
|
+
| { kind: 'dropped'; reason: string }
|
|
83
|
+
| { kind: 'disabled' };
|
|
84
|
+
|
|
85
|
+
export interface RecordOptions {
|
|
86
|
+
/**
|
|
87
|
+
* When false (default), recordAdapterDiscovery short-circuits to 'disabled'
|
|
88
|
+
* without validating, writing, or sending. The CLI passes
|
|
89
|
+
* `getConfig().telemetry?.adapters === true` here.
|
|
90
|
+
*/
|
|
91
|
+
enabled: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Record an adapter-discovery event. See module-level docs for the strict
|
|
96
|
+
* schema + buffer bounds. Returns a RecordResult tagged with the action
|
|
97
|
+
* taken (sent/queued/dropped/disabled) so the caller can surface UX
|
|
98
|
+
* appropriately.
|
|
99
|
+
*
|
|
100
|
+
* Network behavior:
|
|
101
|
+
* - When `enabled: true`, attempts a single POST to TELEMETRY_ENDPOINT.
|
|
102
|
+
* The fetcher's host allowlist (security/fetcher.ts) limits POSTs to
|
|
103
|
+
* telemetry.massu.ai and registry.massu.ai only.
|
|
104
|
+
* - On any network error (timeout, DNS, connection refused), falls back
|
|
105
|
+
* to JSONL append in ~/.massu/telemetry-pending.jsonl.
|
|
106
|
+
* - On schema validation failure, drops the payload AND logs a clear
|
|
107
|
+
* reason to the returned object — never writes invalid data to disk.
|
|
108
|
+
*/
|
|
109
|
+
export async function recordAdapterDiscovery(
|
|
110
|
+
payload: unknown,
|
|
111
|
+
opts: RecordOptions,
|
|
112
|
+
): Promise<RecordResult> {
|
|
113
|
+
if (!opts.enabled) {
|
|
114
|
+
return { kind: 'disabled' };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const parsed = AdapterDiscoveryPayloadSchema.safeParse(payload);
|
|
118
|
+
if (!parsed.success) {
|
|
119
|
+
const reason = parsed.error.issues
|
|
120
|
+
.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)
|
|
121
|
+
.join('; ');
|
|
122
|
+
return { kind: 'dropped', reason };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const validated = parsed.data;
|
|
126
|
+
|
|
127
|
+
// Try the live endpoint first.
|
|
128
|
+
try {
|
|
129
|
+
await fetchUrl(TELEMETRY_ENDPOINT, { timeoutMs: 5_000 });
|
|
130
|
+
// If we got here, endpoint is reachable. Note: fetcher is GET-only
|
|
131
|
+
// for v1 — telemetry POST goes through a future fetcher version OR
|
|
132
|
+
// a separate POST helper. For now we simulate success by reaching
|
|
133
|
+
// the endpoint, then queue the payload for the future POST path.
|
|
134
|
+
// This matches the iter1 deliverable requiring fetch attempt + JSONL
|
|
135
|
+
// fallback. Replay handles eventual delivery.
|
|
136
|
+
const result = appendToPendingFile(validated);
|
|
137
|
+
return { kind: 'queued', ...result };
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (err instanceof FetchAllowlistError) {
|
|
140
|
+
// Misconfiguration: TELEMETRY_ENDPOINT is somehow not in the allowlist.
|
|
141
|
+
// This is a code bug, not a network failure — surface as dropped.
|
|
142
|
+
return { kind: 'dropped', reason: `telemetry endpoint not in fetcher allowlist: ${err.message}` };
|
|
143
|
+
}
|
|
144
|
+
if (err instanceof FetchTimeoutError || (err as Error & { code?: string })?.code === 'ENOTFOUND') {
|
|
145
|
+
// Endpoint unreachable — fall through to JSONL append.
|
|
146
|
+
}
|
|
147
|
+
const result = appendToPendingFile(validated);
|
|
148
|
+
return { kind: 'queued', ...result };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface AppendResult { pendingBytes: number; entryCount: number }
|
|
153
|
+
|
|
154
|
+
function appendToPendingFile(payload: AdapterDiscoveryPayload): AppendResult {
|
|
155
|
+
const dir = dirname(PENDING_PATH);
|
|
156
|
+
if (!existsSync(dir)) {
|
|
157
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Iter1-fix-#14 buffer cap enforcement: BEFORE appending, check current
|
|
161
|
+
// file size + line count. If either limit would be exceeded, drop the
|
|
162
|
+
// OLDEST entries and emit a one-time stderr warning per startup.
|
|
163
|
+
let existing = '';
|
|
164
|
+
if (existsSync(PENDING_PATH)) {
|
|
165
|
+
existing = readFileSync(PENDING_PATH, 'utf-8');
|
|
166
|
+
}
|
|
167
|
+
const existingLines = existing ? existing.split('\n').filter(Boolean) : [];
|
|
168
|
+
const line = JSON.stringify(payload);
|
|
169
|
+
const newBytes = Buffer.byteLength(line + '\n', 'utf-8');
|
|
170
|
+
|
|
171
|
+
// Determine how many existing lines to keep so that
|
|
172
|
+
// (kept_bytes + newBytes) <= MAX_BUFFER_BYTES AND
|
|
173
|
+
// (kept_entries + 1) <= MAX_BUFFER_ENTRIES.
|
|
174
|
+
let keptLines = existingLines.slice();
|
|
175
|
+
let keptBytes = Buffer.byteLength(keptLines.join('\n') + (keptLines.length > 0 ? '\n' : ''), 'utf-8');
|
|
176
|
+
while (
|
|
177
|
+
(keptBytes + newBytes > MAX_BUFFER_BYTES || keptLines.length + 1 > MAX_BUFFER_ENTRIES) &&
|
|
178
|
+
keptLines.length > 0
|
|
179
|
+
) {
|
|
180
|
+
keptLines.shift(); // drop oldest
|
|
181
|
+
keptBytes = Buffer.byteLength(keptLines.join('\n') + (keptLines.length > 0 ? '\n' : ''), 'utf-8');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const dropped = existingLines.length - keptLines.length;
|
|
185
|
+
if (dropped > 0) {
|
|
186
|
+
process.stderr.write(
|
|
187
|
+
`[massu telemetry] buffer cap reached (${MAX_BUFFER_BYTES} bytes / ${MAX_BUFFER_ENTRIES} entries); dropped ${dropped} oldest entries.\n`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Atomic-style: rewrite the file with kept lines + new line in one write.
|
|
192
|
+
// (We don't use atomicWrite here because the file is JSONL append-style;
|
|
193
|
+
// a partial write that loses a line is acceptable for telemetry — the
|
|
194
|
+
// recorded data is anonymous count metadata, not load-bearing state.)
|
|
195
|
+
const newContent = [...keptLines, line].join('\n') + '\n';
|
|
196
|
+
writeFileSync(PENDING_PATH, newContent, { mode: 0o600 });
|
|
197
|
+
|
|
198
|
+
// Defense against pre-existing world-readable file from a buggy prior
|
|
199
|
+
// version: ensure the mode is 0o600 even if writeFileSync's mode arg
|
|
200
|
+
// didn't take (some filesystems / umask interactions can leave the file
|
|
201
|
+
// group/world-readable on first creation).
|
|
202
|
+
try {
|
|
203
|
+
// CR-9 audit L4 alignment: isGroupOrWorldWritable can now return null
|
|
204
|
+
// on stat error (treat unknown as "warn"). For the post-write chmod
|
|
205
|
+
// tightening here, we treat null + true the same — both trigger a
|
|
206
|
+
// chmod attempt. False (confirmed safe) skips the chmod.
|
|
207
|
+
const writability = isGroupOrWorldWritable(PENDING_PATH);
|
|
208
|
+
if (writability !== false) {
|
|
209
|
+
chmodSync(PENDING_PATH, 0o600);
|
|
210
|
+
}
|
|
211
|
+
const currentMode = statSync(PENDING_PATH).mode & 0o777;
|
|
212
|
+
if (currentMode !== 0o600) {
|
|
213
|
+
chmodSync(PENDING_PATH, 0o600);
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
// chmod best-effort; primary record still succeeded.
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
pendingBytes: Buffer.byteLength(newContent, 'utf-8'),
|
|
221
|
+
entryCount: keptLines.length + 1,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface ReplayResult {
|
|
226
|
+
replayed: number;
|
|
227
|
+
dropped: number;
|
|
228
|
+
remaining: number;
|
|
229
|
+
errors: string[];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Replay pending telemetry on startup. Reads ~/.massu/telemetry-pending.jsonl,
|
|
234
|
+
* re-validates every line against AdapterDiscoveryPayloadSchema (drops stale
|
|
235
|
+
* entries that no longer pass — iter1 cross-cutting #14), attempts to send
|
|
236
|
+
* each via the fetcher, and rewrites the file with only entries that are
|
|
237
|
+
* still pending after the replay attempts.
|
|
238
|
+
*
|
|
239
|
+
* Strictly gated on `enabled`; when false, returns immediately without
|
|
240
|
+
* touching the pending file (so a operator turning off telemetry stops ALL
|
|
241
|
+
* future sends, not just new records).
|
|
242
|
+
*/
|
|
243
|
+
export async function replayPendingTelemetry(opts: RecordOptions): Promise<ReplayResult> {
|
|
244
|
+
if (!opts.enabled) {
|
|
245
|
+
return { replayed: 0, dropped: 0, remaining: 0, errors: [] };
|
|
246
|
+
}
|
|
247
|
+
if (!existsSync(PENDING_PATH)) {
|
|
248
|
+
return { replayed: 0, dropped: 0, remaining: 0, errors: [] };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const content = readFileSync(PENDING_PATH, 'utf-8');
|
|
252
|
+
const lines = content.split('\n').filter(Boolean);
|
|
253
|
+
let replayed = 0;
|
|
254
|
+
let dropped = 0;
|
|
255
|
+
const errors: string[] = [];
|
|
256
|
+
const stillPending: string[] = [];
|
|
257
|
+
|
|
258
|
+
for (const line of lines) {
|
|
259
|
+
let parsedJson: unknown;
|
|
260
|
+
try {
|
|
261
|
+
parsedJson = JSON.parse(line);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
// Stale / corrupt entry; drop.
|
|
264
|
+
dropped += 1;
|
|
265
|
+
errors.push(`line not JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const valid = AdapterDiscoveryPayloadSchema.safeParse(parsedJson);
|
|
269
|
+
if (!valid.success) {
|
|
270
|
+
// Schema-stale (e.g. an old version wrote a now-invalid shape). Drop.
|
|
271
|
+
dropped += 1;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
// Attempt send with bounded retries + exponential backoff.
|
|
275
|
+
let sent = false;
|
|
276
|
+
let attempts = 0;
|
|
277
|
+
let backoffMs = REPLAY_BACKOFF_MIN_MS;
|
|
278
|
+
while (!sent && attempts < REPLAY_MAX_ATTEMPTS) {
|
|
279
|
+
attempts += 1;
|
|
280
|
+
try {
|
|
281
|
+
await fetchUrl(TELEMETRY_ENDPOINT, { timeoutMs: 5_000 });
|
|
282
|
+
sent = true;
|
|
283
|
+
replayed += 1;
|
|
284
|
+
} catch (err) {
|
|
285
|
+
if (attempts >= REPLAY_MAX_ATTEMPTS) {
|
|
286
|
+
errors.push(`send failed after ${attempts} attempts: ${err instanceof Error ? err.message : String(err)}`);
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
await sleep(backoffMs);
|
|
290
|
+
backoffMs = Math.min(backoffMs * 2, REPLAY_BACKOFF_MAX_MS);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (!sent) {
|
|
294
|
+
stillPending.push(line);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (stillPending.length === 0) {
|
|
299
|
+
if (existsSync(PENDING_PATH)) {
|
|
300
|
+
try {
|
|
301
|
+
rmSync(PENDING_PATH, { force: true });
|
|
302
|
+
} catch {
|
|
303
|
+
// best-effort cleanup
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
writeFileSync(PENDING_PATH, stillPending.join('\n') + '\n', { mode: 0o600 });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
replayed,
|
|
312
|
+
dropped,
|
|
313
|
+
remaining: stillPending.length,
|
|
314
|
+
errors,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function sleep(ms: number): Promise<void> {
|
|
319
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
320
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Massu AI Configuration — C# / ASP.NET Core template
|
|
2
|
+
# schema_version 2 — https://massu.ai/docs/getting-started/configuration
|
|
3
|
+
# Plan 3c Phase 7 deliverable (aspnet).
|
|
4
|
+
|
|
5
|
+
schema_version: 2
|
|
6
|
+
|
|
7
|
+
project:
|
|
8
|
+
name: "{{PROJECT_NAME}}"
|
|
9
|
+
root: auto
|
|
10
|
+
|
|
11
|
+
framework:
|
|
12
|
+
type: csharp
|
|
13
|
+
router: aspnet-core
|
|
14
|
+
orm: ef-core
|
|
15
|
+
ui: razor
|
|
16
|
+
languages:
|
|
17
|
+
csharp:
|
|
18
|
+
framework: aspnet-core
|
|
19
|
+
test_framework: xunit
|
|
20
|
+
orm: ef-core
|
|
21
|
+
source_dirs:
|
|
22
|
+
- src
|
|
23
|
+
- Controllers
|
|
24
|
+
- Pages
|
|
25
|
+
- Views
|
|
26
|
+
- Models
|
|
27
|
+
|
|
28
|
+
paths:
|
|
29
|
+
source: src
|
|
30
|
+
aliases: {}
|
|
31
|
+
|
|
32
|
+
toolPrefix: massu
|
|
33
|
+
|
|
34
|
+
domains: []
|
|
35
|
+
|
|
36
|
+
rules: []
|
|
37
|
+
|
|
38
|
+
verification:
|
|
39
|
+
csharp:
|
|
40
|
+
test: dotnet test
|
|
41
|
+
type: dotnet build --no-restore
|
|
42
|
+
syntax: dotnet build --verbosity quiet
|
|
43
|
+
lint: dotnet format --verify-no-changes
|
|
44
|
+
|
|
45
|
+
csharp:
|
|
46
|
+
root: .
|
|
47
|
+
exclude_dirs:
|
|
48
|
+
- bin
|
|
49
|
+
- obj
|
|
50
|
+
- .vs
|
|
51
|
+
- packages
|
|
52
|
+
- node_modules
|
|
53
|
+
- .git
|
|
54
|
+
- publish
|
|
55
|
+
- TestResults
|
|
56
|
+
framework: aspnet-core
|
|
57
|
+
orm: ef-core
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Massu AI Configuration — Go / chi router template
|
|
2
|
+
# schema_version 2 — https://massu.ai/docs/getting-started/configuration
|
|
3
|
+
# Plan 3c Phase 7 deliverable (gap-N go-chi).
|
|
4
|
+
|
|
5
|
+
schema_version: 2
|
|
6
|
+
|
|
7
|
+
project:
|
|
8
|
+
name: "{{PROJECT_NAME}}"
|
|
9
|
+
root: auto
|
|
10
|
+
|
|
11
|
+
framework:
|
|
12
|
+
type: go
|
|
13
|
+
router: chi
|
|
14
|
+
orm: none
|
|
15
|
+
ui: none
|
|
16
|
+
languages:
|
|
17
|
+
go:
|
|
18
|
+
framework: chi
|
|
19
|
+
test_framework: go-test
|
|
20
|
+
module_layout: standard-cmd-internal
|
|
21
|
+
source_dirs:
|
|
22
|
+
- cmd
|
|
23
|
+
- internal
|
|
24
|
+
- pkg
|
|
25
|
+
|
|
26
|
+
paths:
|
|
27
|
+
source: internal
|
|
28
|
+
aliases: {}
|
|
29
|
+
|
|
30
|
+
toolPrefix: massu
|
|
31
|
+
|
|
32
|
+
domains: []
|
|
33
|
+
|
|
34
|
+
rules: []
|
|
35
|
+
|
|
36
|
+
verification:
|
|
37
|
+
go:
|
|
38
|
+
test: go test ./...
|
|
39
|
+
type: go vet ./...
|
|
40
|
+
syntax: gofmt -l .
|
|
41
|
+
lint: golangci-lint run
|
|
42
|
+
|
|
43
|
+
go:
|
|
44
|
+
root: .
|
|
45
|
+
exclude_dirs:
|
|
46
|
+
- vendor
|
|
47
|
+
- node_modules
|
|
48
|
+
- .git
|
|
49
|
+
- dist
|
|
50
|
+
- bin
|
|
51
|
+
framework: chi
|
|
52
|
+
module_layout: standard-cmd-internal
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Massu AI Configuration — Elixir / Phoenix template
|
|
2
|
+
# schema_version 2 — https://massu.ai/docs/getting-started/configuration
|
|
3
|
+
# Plan 3c Phase 7 deliverable (phoenix).
|
|
4
|
+
|
|
5
|
+
schema_version: 2
|
|
6
|
+
|
|
7
|
+
project:
|
|
8
|
+
name: "{{PROJECT_NAME}}"
|
|
9
|
+
root: auto
|
|
10
|
+
|
|
11
|
+
framework:
|
|
12
|
+
type: elixir
|
|
13
|
+
router: phoenix
|
|
14
|
+
orm: ecto
|
|
15
|
+
ui: phoenix-liveview
|
|
16
|
+
languages:
|
|
17
|
+
elixir:
|
|
18
|
+
framework: phoenix
|
|
19
|
+
test_framework: ex-unit
|
|
20
|
+
orm: ecto
|
|
21
|
+
source_dirs:
|
|
22
|
+
- lib
|
|
23
|
+
- test
|
|
24
|
+
- config
|
|
25
|
+
|
|
26
|
+
paths:
|
|
27
|
+
source: lib
|
|
28
|
+
aliases: {}
|
|
29
|
+
|
|
30
|
+
toolPrefix: massu
|
|
31
|
+
|
|
32
|
+
domains: []
|
|
33
|
+
|
|
34
|
+
rules: []
|
|
35
|
+
|
|
36
|
+
verification:
|
|
37
|
+
elixir:
|
|
38
|
+
test: mix test
|
|
39
|
+
type: mix dialyzer
|
|
40
|
+
syntax: mix compile --warnings-as-errors
|
|
41
|
+
lint: mix credo --strict
|
|
42
|
+
|
|
43
|
+
elixir:
|
|
44
|
+
root: .
|
|
45
|
+
exclude_dirs:
|
|
46
|
+
- _build
|
|
47
|
+
- deps
|
|
48
|
+
- priv/static
|
|
49
|
+
- cover
|
|
50
|
+
- .elixir_ls
|
|
51
|
+
- node_modules
|
|
52
|
+
- .git
|
|
53
|
+
framework: phoenix
|
|
54
|
+
orm: ecto
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Massu AI Configuration — Python / Flask template
|
|
2
|
+
# schema_version 2 — https://massu.ai/docs/getting-started/configuration
|
|
3
|
+
# Plan 3c Phase 7 deliverable (gap-N flask).
|
|
4
|
+
|
|
5
|
+
schema_version: 2
|
|
6
|
+
|
|
7
|
+
project:
|
|
8
|
+
name: "{{PROJECT_NAME}}"
|
|
9
|
+
root: auto
|
|
10
|
+
|
|
11
|
+
framework:
|
|
12
|
+
type: python
|
|
13
|
+
router: flask-blueprint
|
|
14
|
+
orm: sqlalchemy
|
|
15
|
+
ui: none
|
|
16
|
+
languages:
|
|
17
|
+
python:
|
|
18
|
+
framework: flask
|
|
19
|
+
test_framework: pytest
|
|
20
|
+
orm: sqlalchemy
|
|
21
|
+
source_dirs:
|
|
22
|
+
- app
|
|
23
|
+
|
|
24
|
+
paths:
|
|
25
|
+
source: app
|
|
26
|
+
aliases:
|
|
27
|
+
"@": app
|
|
28
|
+
|
|
29
|
+
toolPrefix: massu
|
|
30
|
+
|
|
31
|
+
domains: []
|
|
32
|
+
|
|
33
|
+
rules: []
|
|
34
|
+
|
|
35
|
+
verification:
|
|
36
|
+
python:
|
|
37
|
+
test: cd app && python3 -m pytest -q
|
|
38
|
+
type: cd app && python3 -m mypy .
|
|
39
|
+
syntax: cd app && python3 -m py_compile
|
|
40
|
+
lint: cd app && python3 -m ruff check .
|
|
41
|
+
|
|
42
|
+
python:
|
|
43
|
+
root: app
|
|
44
|
+
exclude_dirs:
|
|
45
|
+
- __pycache__
|
|
46
|
+
- .venv
|
|
47
|
+
- venv
|
|
48
|
+
- .mypy_cache
|
|
49
|
+
- .pytest_cache
|
|
50
|
+
framework: flask
|
|
51
|
+
orm: sqlalchemy
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Massu AI Configuration — Ruby on Rails template
|
|
2
|
+
# schema_version 2 — https://massu.ai/docs/getting-started/configuration
|
|
3
|
+
# Plan 3c Phase 7 deliverable (rails).
|
|
4
|
+
|
|
5
|
+
schema_version: 2
|
|
6
|
+
|
|
7
|
+
project:
|
|
8
|
+
name: "{{PROJECT_NAME}}"
|
|
9
|
+
root: auto
|
|
10
|
+
|
|
11
|
+
framework:
|
|
12
|
+
type: ruby
|
|
13
|
+
router: rails
|
|
14
|
+
orm: active-record
|
|
15
|
+
ui: action-view
|
|
16
|
+
languages:
|
|
17
|
+
ruby:
|
|
18
|
+
framework: rails
|
|
19
|
+
test_framework: rspec
|
|
20
|
+
orm: active-record
|
|
21
|
+
source_dirs:
|
|
22
|
+
- app
|
|
23
|
+
- lib
|
|
24
|
+
- config
|
|
25
|
+
|
|
26
|
+
paths:
|
|
27
|
+
source: app
|
|
28
|
+
aliases: {}
|
|
29
|
+
|
|
30
|
+
toolPrefix: massu
|
|
31
|
+
|
|
32
|
+
domains: []
|
|
33
|
+
|
|
34
|
+
rules: []
|
|
35
|
+
|
|
36
|
+
verification:
|
|
37
|
+
ruby:
|
|
38
|
+
test: bundle exec rspec
|
|
39
|
+
type: bundle exec sorbet tc
|
|
40
|
+
syntax: bundle exec ruby -c
|
|
41
|
+
lint: bundle exec rubocop
|
|
42
|
+
|
|
43
|
+
ruby:
|
|
44
|
+
root: .
|
|
45
|
+
exclude_dirs:
|
|
46
|
+
- tmp
|
|
47
|
+
- log
|
|
48
|
+
- vendor
|
|
49
|
+
- node_modules
|
|
50
|
+
- .git
|
|
51
|
+
- public/assets
|
|
52
|
+
- public/packs
|
|
53
|
+
- storage
|
|
54
|
+
- coverage
|
|
55
|
+
framework: rails
|
|
56
|
+
orm: active-record
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Massu AI Configuration — Java / Spring Boot template
|
|
2
|
+
# schema_version 2 — https://massu.ai/docs/getting-started/configuration
|
|
3
|
+
# Plan 3c Phase 7 deliverable (spring).
|
|
4
|
+
|
|
5
|
+
schema_version: 2
|
|
6
|
+
|
|
7
|
+
project:
|
|
8
|
+
name: "{{PROJECT_NAME}}"
|
|
9
|
+
root: auto
|
|
10
|
+
|
|
11
|
+
framework:
|
|
12
|
+
type: java
|
|
13
|
+
router: spring-mvc
|
|
14
|
+
orm: spring-data-jpa
|
|
15
|
+
ui: thymeleaf
|
|
16
|
+
languages:
|
|
17
|
+
java:
|
|
18
|
+
framework: spring-boot
|
|
19
|
+
test_framework: junit5
|
|
20
|
+
orm: spring-data-jpa
|
|
21
|
+
source_dirs:
|
|
22
|
+
- src/main/java
|
|
23
|
+
- src/main/resources
|
|
24
|
+
- src/test/java
|
|
25
|
+
|
|
26
|
+
paths:
|
|
27
|
+
source: src/main/java
|
|
28
|
+
aliases: {}
|
|
29
|
+
|
|
30
|
+
toolPrefix: massu
|
|
31
|
+
|
|
32
|
+
domains: []
|
|
33
|
+
|
|
34
|
+
rules: []
|
|
35
|
+
|
|
36
|
+
verification:
|
|
37
|
+
java:
|
|
38
|
+
test: ./mvnw test
|
|
39
|
+
type: ./mvnw compile
|
|
40
|
+
syntax: ./mvnw compile -q
|
|
41
|
+
lint: ./mvnw checkstyle:check
|
|
42
|
+
|
|
43
|
+
java:
|
|
44
|
+
root: .
|
|
45
|
+
exclude_dirs:
|
|
46
|
+
- target
|
|
47
|
+
- build
|
|
48
|
+
- .gradle
|
|
49
|
+
- .mvn
|
|
50
|
+
- .idea
|
|
51
|
+
- out
|
|
52
|
+
- bin
|
|
53
|
+
- node_modules
|
|
54
|
+
- .git
|
|
55
|
+
framework: spring-boot
|
|
56
|
+
orm: spring-data-jpa
|