pulse-framework-cli 0.4.1
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/commands/checkpoint.d.ts +2 -0
- package/dist/commands/checkpoint.js +129 -0
- package/dist/commands/correct.d.ts +2 -0
- package/dist/commands/correct.js +77 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +183 -0
- package/dist/commands/escalate.d.ts +2 -0
- package/dist/commands/escalate.js +226 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +570 -0
- package/dist/commands/learn.d.ts +2 -0
- package/dist/commands/learn.js +137 -0
- package/dist/commands/profile.d.ts +2 -0
- package/dist/commands/profile.js +39 -0
- package/dist/commands/reset.d.ts +2 -0
- package/dist/commands/reset.js +130 -0
- package/dist/commands/review.d.ts +2 -0
- package/dist/commands/review.js +129 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +272 -0
- package/dist/commands/start.d.ts +2 -0
- package/dist/commands/start.js +196 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +239 -0
- package/dist/commands/watch.d.ts +2 -0
- package/dist/commands/watch.js +98 -0
- package/dist/hooks/install.d.ts +1 -0
- package/dist/hooks/install.js +89 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +40 -0
- package/dist/lib/artifacts.d.ts +7 -0
- package/dist/lib/artifacts.js +52 -0
- package/dist/lib/briefing.d.ts +77 -0
- package/dist/lib/briefing.js +231 -0
- package/dist/lib/clipboard.d.ts +9 -0
- package/dist/lib/clipboard.js +116 -0
- package/dist/lib/config.d.ts +14 -0
- package/dist/lib/config.js +167 -0
- package/dist/lib/context-export.d.ts +30 -0
- package/dist/lib/context-export.js +149 -0
- package/dist/lib/exec.d.ts +9 -0
- package/dist/lib/exec.js +23 -0
- package/dist/lib/git.d.ts +24 -0
- package/dist/lib/git.js +74 -0
- package/dist/lib/input.d.ts +15 -0
- package/dist/lib/input.js +80 -0
- package/dist/lib/notifications.d.ts +2 -0
- package/dist/lib/notifications.js +25 -0
- package/dist/lib/paths.d.ts +4 -0
- package/dist/lib/paths.js +39 -0
- package/dist/lib/prompts.d.ts +43 -0
- package/dist/lib/prompts.js +270 -0
- package/dist/lib/scanner.d.ts +37 -0
- package/dist/lib/scanner.js +413 -0
- package/dist/lib/types.d.ts +37 -0
- package/dist/lib/types.js +2 -0
- package/package.json +42 -0
- package/templates/.cursorrules +159 -0
- package/templates/AGENTS.md +198 -0
- package/templates/cursor/mcp.json +9 -0
- package/templates/cursor/pulse.mdc +144 -0
- package/templates/roles/architect.cursorrules +15 -0
- package/templates/roles/backend.cursorrules +12 -0
- package/templates/roles/frontend.cursorrules +12 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.scanDiff = scanDiff;
|
|
7
|
+
exports.detectLoopSignals = detectLoopSignals;
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
function scanDiff(config, input) {
|
|
10
|
+
const findings = [];
|
|
11
|
+
const num = parseNumstat(input.diffNumstat);
|
|
12
|
+
const deletedFiles = parseDeletedFiles(input.diffNameStatus);
|
|
13
|
+
const touchedFiles = num.map((n) => n.file);
|
|
14
|
+
const stats = {
|
|
15
|
+
filesChanged: touchedFiles.length,
|
|
16
|
+
linesAdded: num.reduce((a, n) => a + n.added, 0),
|
|
17
|
+
linesDeleted: num.reduce((a, n) => a + n.deleted, 0),
|
|
18
|
+
deletedFiles,
|
|
19
|
+
touchedFiles,
|
|
20
|
+
};
|
|
21
|
+
// --- Critical: secrets ---
|
|
22
|
+
const secretHits = matchAny(config.patterns.secret, input.diffText);
|
|
23
|
+
if (secretHits.length) {
|
|
24
|
+
findings.push({
|
|
25
|
+
severity: "critical",
|
|
26
|
+
code: "SECRETS",
|
|
27
|
+
message: `Possible secrets found in diff (${secretHits.length} hit(s)).`,
|
|
28
|
+
details: secretHits.slice(0, 5).join("\n"),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// --- Warn: production URLs ---
|
|
32
|
+
const prodUrlHits = matchAny(config.patterns.prodUrl, input.diffText).filter((m) => !m.includes("localhost") && !m.includes("127.0.0.1"));
|
|
33
|
+
if (prodUrlHits.length) {
|
|
34
|
+
findings.push({
|
|
35
|
+
severity: config.enforcement === "strict" ? "critical" : "warn",
|
|
36
|
+
code: "PROD_URL",
|
|
37
|
+
message: `Production/external URL(s) detected in diff (${prodUrlHits.length} hit(s)). Prefer env vars.`,
|
|
38
|
+
details: prodUrlHits.slice(0, 5).join("\n"),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
// --- Critical/Warn: mass deletes (DELETE safeguard) ---
|
|
42
|
+
if (deletedFiles.length) {
|
|
43
|
+
findings.push({
|
|
44
|
+
severity: config.enforcement === "advisory" ? "warn" : "critical",
|
|
45
|
+
code: "MASS_DELETE",
|
|
46
|
+
message: `File deletion detected (${deletedFiles.length} file(s)). DELETE requires explicit confirmation.`,
|
|
47
|
+
details: deletedFiles.slice(0, 10).join("\n"),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
else if (stats.linesDeleted >= config.thresholds.warnMaxDeletions) {
|
|
51
|
+
findings.push({
|
|
52
|
+
severity: "warn",
|
|
53
|
+
code: "MASS_DELETE",
|
|
54
|
+
message: `High deletions in diff (${stats.linesDeleted}). Consider smaller milestones / checkpoint.`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// --- Warn: big changeset (red flag: too much at once) ---
|
|
58
|
+
if (stats.filesChanged >= config.thresholds.warnMaxFilesChanged ||
|
|
59
|
+
stats.linesAdded + stats.linesDeleted >= config.thresholds.warnMaxLinesChanged) {
|
|
60
|
+
findings.push({
|
|
61
|
+
severity: "warn",
|
|
62
|
+
code: "BIG_CHANGESET",
|
|
63
|
+
message: `Large changeset (files=${stats.filesChanged}, lines=${stats.linesAdded + stats.linesDeleted}). Consider smaller milestones.`,
|
|
64
|
+
details: input.diffStat || undefined,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// --- Warn: dependencies changed (unknown deps) ---
|
|
68
|
+
const depsTouched = touchedFiles.some((f) => ["package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock"].includes(node_path_1.default.basename(f)));
|
|
69
|
+
if (depsTouched) {
|
|
70
|
+
// Parse NEW dependencies from diff
|
|
71
|
+
const newDeps = extractNewDependencies(input.diffText);
|
|
72
|
+
if (newDeps.length > 0) {
|
|
73
|
+
findings.push({
|
|
74
|
+
severity: "warn",
|
|
75
|
+
code: "UNKNOWN_DEPS",
|
|
76
|
+
message: `New dependencies detected (${newDeps.length}): Do you know these?`,
|
|
77
|
+
details: newDeps.slice(0, 10).map((d) => ` + ${d.name}${d.version ? ` @ ${d.version}` : ""}`).join("\n"),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
findings.push({
|
|
82
|
+
severity: "warn",
|
|
83
|
+
code: "UNKNOWN_DEPS",
|
|
84
|
+
message: "Dependency/lockfile change detected. Check if changes are intended.",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// --- Warn: console.log / debug leftovers ---
|
|
89
|
+
const consoleHits = simpleLineHits(input.diffText, /^\+\s*console\.log\(/gm, 5);
|
|
90
|
+
if (consoleHits.length) {
|
|
91
|
+
findings.push({
|
|
92
|
+
severity: "warn",
|
|
93
|
+
code: "CONSOLE_LOG",
|
|
94
|
+
message: "console.log detected in added lines. Remove debug output before merging.",
|
|
95
|
+
details: consoleHits.join("\n"),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// --- Warn: commented-out code (very heuristic) ---
|
|
99
|
+
const commentedHits = simpleLineHits(input.diffText, /^\+\s*\/\/\s*(if|for|while|return|const|let|function|class)\b/gim, 5);
|
|
100
|
+
if (commentedHits.length) {
|
|
101
|
+
findings.push({
|
|
102
|
+
severity: "warn",
|
|
103
|
+
code: "COMMENTED_CODE",
|
|
104
|
+
message: "Commented-out code detected in added lines. Prefer deleting or tracking via issue.",
|
|
105
|
+
details: commentedHits.join("\n"),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// --- Warn: TODO without issue ref ---
|
|
109
|
+
const todoHits = simpleLineHits(input.diffText, /^\+\s*\/\/\s*TODO\b(?!.*#\d+)(?!.*CU-)(?!.*JIRA-)/gim, 5);
|
|
110
|
+
if (todoHits.length) {
|
|
111
|
+
findings.push({
|
|
112
|
+
severity: "warn",
|
|
113
|
+
code: "TODO_NO_ISSUE",
|
|
114
|
+
message: "TODO comment without issue reference detected. Add ticket reference to avoid orphan TODOs.",
|
|
115
|
+
details: todoHits.join("\n"),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return { findings, stats };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Extract NEW dependencies from git diff of package.json
|
|
122
|
+
* Looks for added lines with "package": "version" pattern
|
|
123
|
+
*/
|
|
124
|
+
function extractNewDependencies(diffText) {
|
|
125
|
+
const deps = [];
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
// Match added lines in package.json that look like dependencies
|
|
128
|
+
// Pattern: + "package-name": "^1.2.3"
|
|
129
|
+
const depLineRegex = /^\+\s*"(@?[\w\-./]+)"\s*:\s*"([^"]+)"/gm;
|
|
130
|
+
let match;
|
|
131
|
+
while ((match = depLineRegex.exec(diffText)) !== null) {
|
|
132
|
+
const name = match[1] ?? "";
|
|
133
|
+
const version = match[2] ?? "";
|
|
134
|
+
// Skip common non-dependency fields
|
|
135
|
+
if (isNonDependencyField(name))
|
|
136
|
+
continue;
|
|
137
|
+
// Skip if it looks like a lockfile internal entry
|
|
138
|
+
if (name.startsWith("node_modules/"))
|
|
139
|
+
continue;
|
|
140
|
+
// Must look like a valid npm package name
|
|
141
|
+
// - Starts with @ (scoped) or a letter
|
|
142
|
+
// - Contains only valid chars
|
|
143
|
+
// - Version looks like a semver range
|
|
144
|
+
if (!isValidPackageName(name))
|
|
145
|
+
continue;
|
|
146
|
+
if (!isValidVersionRange(version))
|
|
147
|
+
continue;
|
|
148
|
+
// Dedupe
|
|
149
|
+
if (seen.has(name))
|
|
150
|
+
continue;
|
|
151
|
+
seen.add(name);
|
|
152
|
+
deps.push({ name, version });
|
|
153
|
+
}
|
|
154
|
+
return deps;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Check if name looks like a valid npm package name
|
|
158
|
+
*/
|
|
159
|
+
function isValidPackageName(name) {
|
|
160
|
+
// Scoped packages: @scope/name
|
|
161
|
+
if (name.startsWith("@")) {
|
|
162
|
+
return /^@[\w-]+\/[\w.-]+$/.test(name);
|
|
163
|
+
}
|
|
164
|
+
// Regular packages: name or name-with-dashes
|
|
165
|
+
return /^[a-z][\w.-]*$/.test(name);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Check if version looks like a semver range
|
|
169
|
+
*/
|
|
170
|
+
function isValidVersionRange(version) {
|
|
171
|
+
// Common patterns: ^1.0.0, ~1.0.0, >=1.0.0, 1.0.0, *, latest
|
|
172
|
+
// Also: npm:package@version, workspace:*
|
|
173
|
+
if (version === "*" || version === "latest" || version === "next")
|
|
174
|
+
return true;
|
|
175
|
+
if (version.startsWith("npm:"))
|
|
176
|
+
return true;
|
|
177
|
+
if (version.startsWith("workspace:"))
|
|
178
|
+
return true;
|
|
179
|
+
if (/^[\^~>=<]?\d/.test(version))
|
|
180
|
+
return true;
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Check if a field name is a common package.json field (not a dependency)
|
|
185
|
+
*/
|
|
186
|
+
function isNonDependencyField(name) {
|
|
187
|
+
const nonDepFields = [
|
|
188
|
+
// package.json fields
|
|
189
|
+
"name", "version", "description", "main", "module", "types", "typings",
|
|
190
|
+
"scripts", "bin", "files", "repository", "keywords", "author", "license",
|
|
191
|
+
"bugs", "homepage", "engines", "private", "workspaces", "publishConfig",
|
|
192
|
+
"type", "exports", "imports", "sideEffects", "browserslist", "eslintConfig",
|
|
193
|
+
"prettier", "jest", "mocha", "nyc", "lint-staged", "husky", "config",
|
|
194
|
+
"peerDependenciesMeta", "bundleDependencies", "optionalDependencies",
|
|
195
|
+
"overrides", "resolutions", "packageManager", "volta", "directories",
|
|
196
|
+
// Script names
|
|
197
|
+
"dev", "build", "start", "test", "lint", "format", "clean", "watch",
|
|
198
|
+
"preinstall", "postinstall", "prepublish", "prepare",
|
|
199
|
+
// Lock file internal fields
|
|
200
|
+
"resolved", "integrity", "dev", "optional", "requires", "dependencies",
|
|
201
|
+
"node", "npm", "funding", "hasInstallScript", "hasShrinkwrap", "deprecated",
|
|
202
|
+
"peer", "engines", "os", "cpu", "libc", "bin", "license",
|
|
203
|
+
// Common config field values
|
|
204
|
+
"preset", "extends", "plugins", "rules", "env", "globals", "parser",
|
|
205
|
+
"parserOptions", "settings", "ignorePatterns",
|
|
206
|
+
];
|
|
207
|
+
// Also skip if it looks like a version range or URL
|
|
208
|
+
if (/^[\d^~<>=*]/.test(name))
|
|
209
|
+
return true;
|
|
210
|
+
if (name.startsWith("http"))
|
|
211
|
+
return true;
|
|
212
|
+
if (name.startsWith("git"))
|
|
213
|
+
return true;
|
|
214
|
+
if (name.startsWith("file:"))
|
|
215
|
+
return true;
|
|
216
|
+
if (name.startsWith("npm:"))
|
|
217
|
+
return true;
|
|
218
|
+
return nonDepFields.includes(name);
|
|
219
|
+
}
|
|
220
|
+
function matchAny(patterns, text) {
|
|
221
|
+
const out = [];
|
|
222
|
+
for (const p of patterns) {
|
|
223
|
+
try {
|
|
224
|
+
const re = new RegExp(p, "g");
|
|
225
|
+
const m = text.match(re);
|
|
226
|
+
if (m)
|
|
227
|
+
out.push(...m.slice(0, 5));
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// ignore invalid patterns
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// de-dupe
|
|
234
|
+
return [...new Set(out)];
|
|
235
|
+
}
|
|
236
|
+
function simpleLineHits(text, re, limit) {
|
|
237
|
+
const hits = [];
|
|
238
|
+
let m;
|
|
239
|
+
while ((m = re.exec(text)) && hits.length < limit) {
|
|
240
|
+
hits.push(m[0]);
|
|
241
|
+
}
|
|
242
|
+
return hits;
|
|
243
|
+
}
|
|
244
|
+
function parseDeletedFiles(nameStatus) {
|
|
245
|
+
const files = [];
|
|
246
|
+
for (const line of nameStatus.split("\n")) {
|
|
247
|
+
const trimmed = line.trim();
|
|
248
|
+
if (!trimmed)
|
|
249
|
+
continue;
|
|
250
|
+
const [status, file] = trimmed.split(/\s+/);
|
|
251
|
+
if (status === "D" && file)
|
|
252
|
+
files.push(file);
|
|
253
|
+
}
|
|
254
|
+
return files;
|
|
255
|
+
}
|
|
256
|
+
function parseNumstat(numstat) {
|
|
257
|
+
const rows = [];
|
|
258
|
+
for (const line of numstat.split("\n")) {
|
|
259
|
+
const trimmed = line.trim();
|
|
260
|
+
if (!trimmed)
|
|
261
|
+
continue;
|
|
262
|
+
const parts = trimmed.split("\t");
|
|
263
|
+
if (parts.length < 3)
|
|
264
|
+
continue;
|
|
265
|
+
const [a, d, file] = parts;
|
|
266
|
+
const added = a === "-" ? 0 : Number(a);
|
|
267
|
+
const deleted = d === "-" ? 0 : Number(d);
|
|
268
|
+
rows.push({ added: Number.isFinite(added) ? added : 0, deleted: Number.isFinite(deleted) ? deleted : 0, file: file ?? "" });
|
|
269
|
+
}
|
|
270
|
+
return rows;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Analyze git log for loop signals
|
|
274
|
+
* @param gitLog Output from `git log --oneline -n 15`
|
|
275
|
+
* @param gitLogWithFiles Output from `git log --name-only --oneline -n 15`
|
|
276
|
+
*/
|
|
277
|
+
function detectLoopSignals(gitLog, gitLogWithFiles) {
|
|
278
|
+
const signals = [];
|
|
279
|
+
const lines = gitLog.split("\n").filter((l) => l.trim());
|
|
280
|
+
const messages = lines.map((l) => l.replace(/^[a-f0-9]+\s+/, "").toLowerCase());
|
|
281
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
282
|
+
// Signal 1: Fix-Chain (multiple "fix" commits in a row)
|
|
283
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
284
|
+
const fixCount = messages.filter((m) => /^fix(\(|:|\s)/i.test(m)).length;
|
|
285
|
+
if (fixCount >= 3) {
|
|
286
|
+
signals.push({
|
|
287
|
+
type: "fix_chain",
|
|
288
|
+
severity: "warn",
|
|
289
|
+
message: `Loop signal: ${fixCount}x "fix" commits in last 15 commits. Possible fix-loop.`,
|
|
290
|
+
details: messages.slice(0, 6).join("\n"),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
294
|
+
// Signal 2: Revert-Pattern (explizite Reverts)
|
|
295
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
296
|
+
const revertCount = messages.filter((m) => /\brevert\b/i.test(m)).length;
|
|
297
|
+
if (revertCount >= 1) {
|
|
298
|
+
signals.push({
|
|
299
|
+
type: "revert",
|
|
300
|
+
severity: revertCount >= 2 ? "critical" : "warn",
|
|
301
|
+
message: `Loop signal: ${revertCount}x "revert" found. A↔B toggling possible.`,
|
|
302
|
+
details: messages.filter((m) => /\brevert\b/i.test(m)).join("\n"),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
306
|
+
// Signal 3: File-Churn (same file changed multiple times in short time)
|
|
307
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
308
|
+
if (gitLogWithFiles) {
|
|
309
|
+
const fileChanges = parseFileChangesFromLog(gitLogWithFiles);
|
|
310
|
+
const churnFiles = Object.entries(fileChanges)
|
|
311
|
+
.filter(([_, count]) => count >= 5)
|
|
312
|
+
.map(([file, count]) => `${file} (${count}x)`);
|
|
313
|
+
if (churnFiles.length > 0) {
|
|
314
|
+
signals.push({
|
|
315
|
+
type: "churn",
|
|
316
|
+
severity: "warn",
|
|
317
|
+
message: `Loop signal: File-Churn - ${churnFiles.length} file(s) changed 5+ times.`,
|
|
318
|
+
details: churnFiles.slice(0, 5).join("\n"),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
323
|
+
// Signal 4: Fix without test changes
|
|
324
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
325
|
+
if (gitLogWithFiles) {
|
|
326
|
+
const fixWithoutTest = detectFixWithoutTest(gitLogWithFiles);
|
|
327
|
+
if (fixWithoutTest.length >= 2) {
|
|
328
|
+
signals.push({
|
|
329
|
+
type: "fix_no_test",
|
|
330
|
+
severity: "warn",
|
|
331
|
+
message: `Loop signal: ${fixWithoutTest.length}x "fix" commits without test changes.`,
|
|
332
|
+
details: fixWithoutTest.slice(0, 3).join("\n"),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
337
|
+
// Signal 5: Pendulum (similar commit messages repeating)
|
|
338
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
339
|
+
const similarMessages = findSimilarMessages(messages);
|
|
340
|
+
if (similarMessages.length > 0) {
|
|
341
|
+
signals.push({
|
|
342
|
+
type: "pendeln",
|
|
343
|
+
severity: "critical",
|
|
344
|
+
message: `Loop signal: Similar commits repeating. Possible diff pendulum.`,
|
|
345
|
+
details: similarMessages.join("\n"),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return signals;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Parse file changes from git log --name-only output
|
|
352
|
+
*/
|
|
353
|
+
function parseFileChangesFromLog(logWithFiles) {
|
|
354
|
+
const fileCount = {};
|
|
355
|
+
const lines = logWithFiles.split("\n");
|
|
356
|
+
for (const line of lines) {
|
|
357
|
+
const trimmed = line.trim();
|
|
358
|
+
// Skip commit hashes and empty lines
|
|
359
|
+
if (!trimmed || /^[a-f0-9]{7,}/.test(trimmed))
|
|
360
|
+
continue;
|
|
361
|
+
// Count file occurrences
|
|
362
|
+
if (trimmed.includes(".") || trimmed.includes("/")) {
|
|
363
|
+
fileCount[trimmed] = (fileCount[trimmed] ?? 0) + 1;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return fileCount;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Detect "fix" commits that don't touch test files
|
|
370
|
+
*/
|
|
371
|
+
function detectFixWithoutTest(logWithFiles) {
|
|
372
|
+
const results = [];
|
|
373
|
+
const blocks = logWithFiles.split(/\n(?=[a-f0-9]{7,})/);
|
|
374
|
+
for (const block of blocks) {
|
|
375
|
+
const lines = block.split("\n").filter((l) => l.trim());
|
|
376
|
+
if (lines.length === 0)
|
|
377
|
+
continue;
|
|
378
|
+
const firstLine = lines[0] ?? "";
|
|
379
|
+
const message = firstLine.replace(/^[a-f0-9]+\s+/, "").toLowerCase();
|
|
380
|
+
// Check if it's a fix commit
|
|
381
|
+
if (!/^fix(\(|:|\s)/i.test(message))
|
|
382
|
+
continue;
|
|
383
|
+
// Check if any files are test files
|
|
384
|
+
const files = lines.slice(1);
|
|
385
|
+
const hasTestFile = files.some((f) => /\.(test|spec)\.[jt]sx?$/.test(f) ||
|
|
386
|
+
/__(tests|test)__/.test(f) ||
|
|
387
|
+
/\.test\./.test(f));
|
|
388
|
+
if (!hasTestFile) {
|
|
389
|
+
results.push(firstLine);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return results;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Find similar commit messages that might indicate pendeln
|
|
396
|
+
*/
|
|
397
|
+
function findSimilarMessages(messages) {
|
|
398
|
+
const similar = [];
|
|
399
|
+
// Simple: Check for near-identical messages
|
|
400
|
+
for (let i = 0; i < messages.length - 1; i++) {
|
|
401
|
+
for (let j = i + 1; j < messages.length; j++) {
|
|
402
|
+
const m1 = messages[i] ?? "";
|
|
403
|
+
const m2 = messages[j] ?? "";
|
|
404
|
+
// Normalize: remove version numbers, timestamps
|
|
405
|
+
const norm1 = m1.replace(/v?\d+(\.\d+)*/g, "").replace(/\s+/g, " ").trim();
|
|
406
|
+
const norm2 = m2.replace(/v?\d+(\.\d+)*/g, "").replace(/\s+/g, " ").trim();
|
|
407
|
+
if (norm1 === norm2 && norm1.length > 10) {
|
|
408
|
+
similar.push(`"${m1}" ≈ "${m2}"`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return similar;
|
|
413
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type PulseLayer = "concept" | "build" | "escalation";
|
|
2
|
+
export type EnforcementMode = "advisory" | "mixed" | "strict";
|
|
3
|
+
export type NotificationMode = "terminal" | "macos" | "both";
|
|
4
|
+
export type ProjectType = "node" | "python" | "unknown";
|
|
5
|
+
export type PresetName = "frontend" | "backend" | "fullstack" | "monorepo" | "custom";
|
|
6
|
+
export type PresetConfig = {
|
|
7
|
+
warnMaxFilesChanged: number;
|
|
8
|
+
warnMaxLinesChanged: number;
|
|
9
|
+
warnMaxDeletions: number;
|
|
10
|
+
checkpointReminderMinutes: number;
|
|
11
|
+
extraSecretPatterns?: string[];
|
|
12
|
+
};
|
|
13
|
+
export type PulseConfig = {
|
|
14
|
+
version: 1;
|
|
15
|
+
projectType: ProjectType;
|
|
16
|
+
enforcement: EnforcementMode;
|
|
17
|
+
notifications: NotificationMode;
|
|
18
|
+
preset?: PresetName;
|
|
19
|
+
thresholds: {
|
|
20
|
+
warnMaxFilesChanged: number;
|
|
21
|
+
warnMaxLinesChanged: number;
|
|
22
|
+
warnMaxDeletions: number;
|
|
23
|
+
};
|
|
24
|
+
patterns: {
|
|
25
|
+
secret: string[];
|
|
26
|
+
prodUrl: string[];
|
|
27
|
+
};
|
|
28
|
+
commands: {
|
|
29
|
+
test?: string;
|
|
30
|
+
};
|
|
31
|
+
checkpointReminderMinutes?: number;
|
|
32
|
+
};
|
|
33
|
+
export type PulseState = {
|
|
34
|
+
version: 1;
|
|
35
|
+
profile: PulseLayer;
|
|
36
|
+
lastCheckpointAt?: string;
|
|
37
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pulse-framework-cli",
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Pulse Framework CLI – Guardrails, checkpoints, and escalation for AI-assisted development.",
|
|
5
|
+
"author": "Manuel Fuß <kontakt@manuel-fuss.de>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://manuel-fuss.de/pulse",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/manuelfussTC/PulseFramework"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"pulse",
|
|
14
|
+
"ai",
|
|
15
|
+
"cursor",
|
|
16
|
+
"agent",
|
|
17
|
+
"guardrails",
|
|
18
|
+
"checkpoint",
|
|
19
|
+
"cli"
|
|
20
|
+
],
|
|
21
|
+
"bin": {
|
|
22
|
+
"pulse": "dist/index.js",
|
|
23
|
+
"pulse-framework": "dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"type": "commonjs",
|
|
26
|
+
"main": "dist/index.js",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"templates"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc -p tsconfig.json",
|
|
33
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
34
|
+
"dev": "tsx src/index.ts"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"chokidar": "^3.6.0",
|
|
38
|
+
"commander": "^12.1.0",
|
|
39
|
+
"glob": "^11.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
# PULSE FRAMEWORK - AI Agent Rules (v1.1)
|
|
3
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
4
|
+
# Author: Manuel Fuß (kontakt@manuel-fuss.de)
|
|
5
|
+
# Repo: https://github.com/manuelfussTC/PulseFramework
|
|
6
|
+
#
|
|
7
|
+
# These rules apply to YOU, the AI Agent. You MUST follow them.
|
|
8
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
9
|
+
|
|
10
|
+
# ┌─────────────────────────────────────────────────────────────────────────────┐
|
|
11
|
+
# │ 🛑 CRITICAL RULE: 30-MINUTE AUTONOMY LIMIT │
|
|
12
|
+
# └─────────────────────────────────────────────────────────────────────────────┘
|
|
13
|
+
#
|
|
14
|
+
# You may work autonomously for a MAXIMUM of 30 minutes. Then you MUST:
|
|
15
|
+
#
|
|
16
|
+
# 1. STOP - No further action without human confirmation
|
|
17
|
+
# 2. SUMMARIZE - What have you done in the last 30 min?
|
|
18
|
+
# 3. ASK - "Should I continue? Here is my current status: [...]"
|
|
19
|
+
#
|
|
20
|
+
# WHY: After 30 min you lose context, build in wrong directions,
|
|
21
|
+
# and overwrite working things.
|
|
22
|
+
#
|
|
23
|
+
# For EVERY longer task:
|
|
24
|
+
# - Split into 30-min blocks
|
|
25
|
+
# - At the end of each block: Checkpoint + Summary + Question
|
|
26
|
+
# - NEVER "just one more quick thing" after 30 min
|
|
27
|
+
|
|
28
|
+
# ┌─────────────────────────────────────────────────────────────────────────────┐
|
|
29
|
+
# │ 🔒 5 CRITICAL SAFEGUARDS - NON-NEGOTIABLE │
|
|
30
|
+
# └─────────────────────────────────────────────────────────────────────────────┘
|
|
31
|
+
|
|
32
|
+
# SAFEGUARD 1: DELETE LOCK
|
|
33
|
+
# - NEVER delete a non-empty file without explicitly asking:
|
|
34
|
+
# "CONFIRM DELETION: [filename] - This file contains [X lines]. Delete?"
|
|
35
|
+
# - NEVER "Apply All" on a diff >50 lines without prior summary
|
|
36
|
+
|
|
37
|
+
# SAFEGUARD 2: GIT PUSH LOCK
|
|
38
|
+
# - NEVER git push without explicit instruction
|
|
39
|
+
# - Before every push: "Ready to push. Changes: [summary]. Confirm push?"
|
|
40
|
+
|
|
41
|
+
# SAFEGUARD 3: DEPLOYMENT LOCK
|
|
42
|
+
# - NEVER deploy without prior local test
|
|
43
|
+
# - Before deploy: "Local tests: [passed/failed]. Confirm deploy?"
|
|
44
|
+
|
|
45
|
+
# SAFEGUARD 4: BREAKING CHANGES
|
|
46
|
+
# - On API changes, schema migrations, config changes:
|
|
47
|
+
# "⚠️ BREAKING CHANGE: [what changes]. Continue?"
|
|
48
|
+
|
|
49
|
+
# SAFEGUARD 5: SECRETS
|
|
50
|
+
# - NEVER put secrets, API keys, tokens in code
|
|
51
|
+
# - If you see a secret: "🚨 SECRET FOUND: [masked]. Please move to .env."
|
|
52
|
+
|
|
53
|
+
# ┌─────────────────────────────────────────────────────────────────────────────┐
|
|
54
|
+
# │ 🔄 LOOP DETECTION - SELF-MONITORING │
|
|
55
|
+
# └─────────────────────────────────────────────────────────────────────────────┘
|
|
56
|
+
|
|
57
|
+
# You MUST monitor yourself for loops:
|
|
58
|
+
|
|
59
|
+
# LOOP TYPE 1: "Is fixed" but not
|
|
60
|
+
# - If you say >2x "now it should work" and it doesn't:
|
|
61
|
+
# - STOP. Say: "I'm stuck in a loop. Here's what I've tried: [...]"
|
|
62
|
+
# - Recommend escalation to a reasoning model
|
|
63
|
+
|
|
64
|
+
# LOOP TYPE 2: Back-and-forth (A↔B)
|
|
65
|
+
# - If you change something, then undo it, then change it again:
|
|
66
|
+
# - STOP. Say: "I'm toggling between two approaches. Which should I choose?"
|
|
67
|
+
|
|
68
|
+
# LOOP TYPE 3: Doesn't understand the problem
|
|
69
|
+
# - If you're unsure what the actual problem is:
|
|
70
|
+
# - STOP. Say: "I'm not sure I understand the problem.
|
|
71
|
+
# Let me summarize what I understood: [...]"
|
|
72
|
+
|
|
73
|
+
# LOOP TYPE 4: Too much at once
|
|
74
|
+
# - If the task is too big:
|
|
75
|
+
# - STOP. Say: "This task is too big for one block.
|
|
76
|
+
# I suggest: Milestone 1: [...], Milestone 2: [...]"
|
|
77
|
+
|
|
78
|
+
# LOOP TYPE 5: Verification loop (status/search/status without action)
|
|
79
|
+
# - If you keep running checks without implementing anything (2 cycles):
|
|
80
|
+
# - STOP. Say: "I'm in verification mode. I'll now implement the smallest concrete change."
|
|
81
|
+
# - Then implement ONE minimal change and re-check once.
|
|
82
|
+
|
|
83
|
+
# ┌─────────────────────────────────────────────────────────────────────────────┐
|
|
84
|
+
# │ 📋 CHECKPOINT PROTOCOL │
|
|
85
|
+
# └─────────────────────────────────────────────────────────────────────────────┘
|
|
86
|
+
|
|
87
|
+
# Every 5-10 minutes (or after every significant change):
|
|
88
|
+
#
|
|
89
|
+
# 1. GIT COMMIT with clear message
|
|
90
|
+
# 2. SHORT SUMMARY: "Checkpoint: [what was done]"
|
|
91
|
+
# 3. NEXT STEP: "Next up: [what comes next]"
|
|
92
|
+
#
|
|
93
|
+
# Commit message format:
|
|
94
|
+
# - feat: New feature
|
|
95
|
+
# - fix: Bug fix
|
|
96
|
+
# - refactor: Code improvement without functionality change
|
|
97
|
+
# - docs: Documentation
|
|
98
|
+
# - test: Tests
|
|
99
|
+
# - chore: Maintenance
|
|
100
|
+
|
|
101
|
+
# ┌─────────────────────────────────────────────────────────────────────────────┐
|
|
102
|
+
# │ 🚨 RED FLAGS - STOP IMMEDIATELY │
|
|
103
|
+
# └─────────────────────────────────────────────────────────────────────────────┘
|
|
104
|
+
|
|
105
|
+
# If you recognize any of these patterns, STOP IMMEDIATELY and report:
|
|
106
|
+
#
|
|
107
|
+
# 🚩 You no longer understand your own code
|
|
108
|
+
# 🚩 More than 200 lines in one change
|
|
109
|
+
# 🚩 Dependencies you don't know
|
|
110
|
+
# 🚩 Deleted files without explicit confirmation
|
|
111
|
+
# 🚩 Production URLs hardcoded
|
|
112
|
+
# 🚩 console.log/print statements for debugging forgotten
|
|
113
|
+
# 🚩 Commented-out code
|
|
114
|
+
# 🚩 TODO without issue reference
|
|
115
|
+
|
|
116
|
+
# ┌─────────────────────────────────────────────────────────────────────────────┐
|
|
117
|
+
# │ 📊 ESCALATION │
|
|
118
|
+
# └─────────────────────────────────────────────────────────────────────────────┘
|
|
119
|
+
|
|
120
|
+
# When you need to escalate, ALWAYS provide:
|
|
121
|
+
#
|
|
122
|
+
# 1. PROBLEM SUMMARY
|
|
123
|
+
# - What was the original task?
|
|
124
|
+
# - What did I try?
|
|
125
|
+
# - What doesn't work?
|
|
126
|
+
#
|
|
127
|
+
# 2. CONTEXT
|
|
128
|
+
# - Relevant code snippets
|
|
129
|
+
# - Error messages (complete)
|
|
130
|
+
# - Git history of recent changes
|
|
131
|
+
#
|
|
132
|
+
# 3. HYPOTHESES
|
|
133
|
+
# - What could be the cause?
|
|
134
|
+
# - What have I already ruled out?
|
|
135
|
+
#
|
|
136
|
+
# 4. SPECIFIC QUESTION
|
|
137
|
+
# - What exactly should the reasoning model answer?
|
|
138
|
+
|
|
139
|
+
# ┌─────────────────────────────────────────────────────────────────────────────┐
|
|
140
|
+
# │ ✅ START PROTOCOL │
|
|
141
|
+
# └─────────────────────────────────────────────────────────────────────────────┘
|
|
142
|
+
|
|
143
|
+
# For EVERY new task:
|
|
144
|
+
#
|
|
145
|
+
# 1. CONFIRM your understanding:
|
|
146
|
+
# "I understand the task as follows: [summary]"
|
|
147
|
+
#
|
|
148
|
+
# 2. PLAN before you code:
|
|
149
|
+
# "My plan: 1. [...] 2. [...] 3. [...]"
|
|
150
|
+
#
|
|
151
|
+
# 3. ASK about uncertainties:
|
|
152
|
+
# "Before I start: [specific question]"
|
|
153
|
+
#
|
|
154
|
+
# 4. ESTIMATE the effort:
|
|
155
|
+
# "Estimated effort: [X] minutes. Should I start?"
|
|
156
|
+
|
|
157
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
158
|
+
# END OF RULES - You are now ready. Follow these rules.
|
|
159
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|