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,239 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerStatusCommand = registerStatusCommand;
|
|
4
|
+
const artifacts_js_1 = require("../lib/artifacts.js");
|
|
5
|
+
const config_js_1 = require("../lib/config.js");
|
|
6
|
+
const paths_js_1 = require("../lib/paths.js");
|
|
7
|
+
const git_js_1 = require("../lib/git.js");
|
|
8
|
+
const scanner_js_1 = require("../lib/scanner.js");
|
|
9
|
+
const git_js_2 = require("../lib/git.js");
|
|
10
|
+
const exec_js_1 = require("../lib/exec.js");
|
|
11
|
+
const briefing_js_1 = require("../lib/briefing.js");
|
|
12
|
+
function registerStatusCommand(program) {
|
|
13
|
+
program
|
|
14
|
+
.command("status")
|
|
15
|
+
.description("Quick overview: Preset/Profile, checkpoint time, changes, findings")
|
|
16
|
+
.option("--json", "Output as JSON")
|
|
17
|
+
.option("-v, --verbose", "Verbose output")
|
|
18
|
+
.option("--share", "Markdown format for Slack/Discord")
|
|
19
|
+
.action(async (opts) => {
|
|
20
|
+
const repoRoot = await (0, paths_js_1.findRepoRoot)(process.cwd());
|
|
21
|
+
if (!repoRoot) {
|
|
22
|
+
if (opts.json) {
|
|
23
|
+
// eslint-disable-next-line no-console
|
|
24
|
+
console.log(JSON.stringify({ error: "Not in a git repository" }));
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
// eslint-disable-next-line no-console
|
|
28
|
+
console.log("❌ Not in a git repository");
|
|
29
|
+
}
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
const [state, config, gitStatus] = await Promise.all([
|
|
33
|
+
(0, artifacts_js_1.loadState)(repoRoot),
|
|
34
|
+
(0, config_js_1.loadConfig)(repoRoot),
|
|
35
|
+
(0, git_js_1.gitStatusPorcelain)(repoRoot),
|
|
36
|
+
]);
|
|
37
|
+
// Calculate time since last checkpoint
|
|
38
|
+
const lastCp = state.lastCheckpointAt ? Date.parse(state.lastCheckpointAt) : null;
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
const minutesSinceCheckpoint = lastCp && Number.isFinite(lastCp) ? Math.floor((now - lastCp) / 60000) : null;
|
|
41
|
+
// Count dirty files
|
|
42
|
+
const dirtyFiles = gitStatus
|
|
43
|
+
.split("\n")
|
|
44
|
+
.filter((line) => line.trim().length > 0).length;
|
|
45
|
+
// Scan for findings (if there are changes)
|
|
46
|
+
let findingsCount = 0;
|
|
47
|
+
let criticalCount = 0;
|
|
48
|
+
let warningCount = 0;
|
|
49
|
+
let linesChanged = 0;
|
|
50
|
+
let loopRisk = "LOW";
|
|
51
|
+
let recommendation = null;
|
|
52
|
+
if (dirtyFiles > 0 || opts.verbose) {
|
|
53
|
+
const [diffText, diffStat, diffNumstat, diffNameStatus, log, logWithFiles] = await Promise.all([
|
|
54
|
+
(0, git_js_2.gitDiffText)(repoRoot),
|
|
55
|
+
(0, git_js_2.gitDiffStat)(repoRoot),
|
|
56
|
+
(0, git_js_2.gitDiffNumstat)(repoRoot),
|
|
57
|
+
(0, git_js_2.gitDiffNameStatus)(repoRoot),
|
|
58
|
+
(0, git_js_1.gitLogOneline)(repoRoot, 15),
|
|
59
|
+
(0, exec_js_1.exec)("git", ["log", "--name-only", "--oneline", "-15"], { cwd: repoRoot }).then((r) => r.stdout),
|
|
60
|
+
]);
|
|
61
|
+
const scan = (0, scanner_js_1.scanDiff)(config, { diffText, diffStat, diffNumstat, diffNameStatus });
|
|
62
|
+
// Add loop signals
|
|
63
|
+
const loopSignals = (0, scanner_js_1.detectLoopSignals)(log, logWithFiles);
|
|
64
|
+
for (const signal of loopSignals) {
|
|
65
|
+
scan.findings.push({
|
|
66
|
+
severity: signal.severity,
|
|
67
|
+
code: "LOOP_SIGNAL",
|
|
68
|
+
message: signal.message,
|
|
69
|
+
details: signal.details,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
findingsCount = scan.findings.length;
|
|
73
|
+
criticalCount = scan.findings.filter((f) => f.severity === "critical").length;
|
|
74
|
+
warningCount = scan.findings.filter((f) => f.severity === "warn").length;
|
|
75
|
+
linesChanged = scan.stats.linesAdded + scan.stats.linesDeleted;
|
|
76
|
+
// Calculate risk for verbose/share
|
|
77
|
+
if (opts.verbose || opts.share) {
|
|
78
|
+
const scope = (0, briefing_js_1.calculateScopeCheck)(config, scan.stats);
|
|
79
|
+
const risk = (0, briefing_js_1.calculateRiskSummary)(scan);
|
|
80
|
+
const time = (0, briefing_js_1.calculateTimeSummary)(state.lastCheckpointAt, config.checkpointReminderMinutes ?? 30);
|
|
81
|
+
recommendation = (0, briefing_js_1.generateRecommendation)(scope, risk, time);
|
|
82
|
+
loopRisk = risk.loopRisk;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Preset/Profile combo
|
|
86
|
+
const presetProfile = config.preset ? `${config.preset}/${state.profile}` : state.profile;
|
|
87
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
88
|
+
// JSON Output
|
|
89
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
90
|
+
if (opts.json) {
|
|
91
|
+
// eslint-disable-next-line no-console
|
|
92
|
+
console.log(JSON.stringify({
|
|
93
|
+
preset: config.preset || null,
|
|
94
|
+
profile: state.profile,
|
|
95
|
+
presetProfile,
|
|
96
|
+
lastCheckpointMinutesAgo: minutesSinceCheckpoint,
|
|
97
|
+
dirtyFiles,
|
|
98
|
+
linesChanged,
|
|
99
|
+
findings: findingsCount,
|
|
100
|
+
criticalFindings: criticalCount,
|
|
101
|
+
warningFindings: warningCount,
|
|
102
|
+
loopRisk,
|
|
103
|
+
recommendation: recommendation?.action || null,
|
|
104
|
+
}));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
108
|
+
// Share Output (Markdown)
|
|
109
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
110
|
+
if (opts.share) {
|
|
111
|
+
const lines = [];
|
|
112
|
+
lines.push(`**PULSE Status**`);
|
|
113
|
+
lines.push(``);
|
|
114
|
+
lines.push(`- Profile: \`${presetProfile}\``);
|
|
115
|
+
lines.push(`- Checkpoint: ${minutesSinceCheckpoint !== null ? `${minutesSinceCheckpoint} min` : "n/a"}`);
|
|
116
|
+
lines.push(`- Files: ${dirtyFiles}`);
|
|
117
|
+
lines.push(`- Lines: ${linesChanged}`);
|
|
118
|
+
lines.push(`- Findings: ${criticalCount} Critical, ${warningCount} Warnings`);
|
|
119
|
+
lines.push(`- Loop risk: ${loopRisk}`);
|
|
120
|
+
if (recommendation) {
|
|
121
|
+
lines.push(``);
|
|
122
|
+
lines.push(`**Recommendation:** ${recommendation.action.toUpperCase()}`);
|
|
123
|
+
lines.push(`> ${recommendation.reason}`);
|
|
124
|
+
}
|
|
125
|
+
// eslint-disable-next-line no-console
|
|
126
|
+
console.log(lines.join("\n"));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
130
|
+
// Verbose Output
|
|
131
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
132
|
+
if (opts.verbose) {
|
|
133
|
+
// eslint-disable-next-line no-console
|
|
134
|
+
console.log(`\n📊 PULSE Status\n`);
|
|
135
|
+
// Profile
|
|
136
|
+
const profileEmoji = state.profile === "concept" ? "🧠" : state.profile === "build" ? "🔨" : "🚨";
|
|
137
|
+
// eslint-disable-next-line no-console
|
|
138
|
+
console.log(`${profileEmoji} Profile: ${presetProfile}`);
|
|
139
|
+
// Checkpoint
|
|
140
|
+
if (minutesSinceCheckpoint !== null) {
|
|
141
|
+
const cpColor = minutesSinceCheckpoint > 30 ? "🔴" : minutesSinceCheckpoint > 15 ? "🟡" : "🟢";
|
|
142
|
+
// eslint-disable-next-line no-console
|
|
143
|
+
console.log(`${cpColor} Checkpoint: ${minutesSinceCheckpoint} min ago`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// eslint-disable-next-line no-console
|
|
147
|
+
console.log(`⚪ Checkpoint: none yet`);
|
|
148
|
+
}
|
|
149
|
+
// Files & Lines
|
|
150
|
+
// eslint-disable-next-line no-console
|
|
151
|
+
console.log(`📝 Files: ${dirtyFiles}`);
|
|
152
|
+
// eslint-disable-next-line no-console
|
|
153
|
+
console.log(`📏 Lines: ${linesChanged}`);
|
|
154
|
+
// Scope bars
|
|
155
|
+
if (dirtyFiles > 0) {
|
|
156
|
+
const filesPercent = Math.round((dirtyFiles / config.thresholds.warnMaxFilesChanged) * 100);
|
|
157
|
+
const linesPercent = Math.round((linesChanged / config.thresholds.warnMaxLinesChanged) * 100);
|
|
158
|
+
// eslint-disable-next-line no-console
|
|
159
|
+
console.log(`\n📊 Scope (${config.preset ?? "custom"} Preset):`);
|
|
160
|
+
// eslint-disable-next-line no-console
|
|
161
|
+
console.log(` Files: ${(0, briefing_js_1.renderProgressBar)(filesPercent)} ${filesPercent}% (${dirtyFiles}/${config.thresholds.warnMaxFilesChanged})`);
|
|
162
|
+
// eslint-disable-next-line no-console
|
|
163
|
+
console.log(` Lines: ${(0, briefing_js_1.renderProgressBar)(linesPercent)} ${linesPercent}% (${linesChanged}/${config.thresholds.warnMaxLinesChanged})`);
|
|
164
|
+
}
|
|
165
|
+
// Findings
|
|
166
|
+
// eslint-disable-next-line no-console
|
|
167
|
+
console.log(`\n🔍 Findings:`);
|
|
168
|
+
if (criticalCount > 0) {
|
|
169
|
+
// eslint-disable-next-line no-console
|
|
170
|
+
console.log(` 🚨 ${criticalCount} Critical`);
|
|
171
|
+
}
|
|
172
|
+
if (warningCount > 0) {
|
|
173
|
+
// eslint-disable-next-line no-console
|
|
174
|
+
console.log(` ⚠️ ${warningCount} Warnings`);
|
|
175
|
+
}
|
|
176
|
+
if (findingsCount === 0) {
|
|
177
|
+
// eslint-disable-next-line no-console
|
|
178
|
+
console.log(` ✅ No findings`);
|
|
179
|
+
}
|
|
180
|
+
// Loop Risk
|
|
181
|
+
const loopEmoji = loopRisk === "HIGH" ? "🔴" : loopRisk === "MEDIUM" ? "🟡" : "🟢";
|
|
182
|
+
// eslint-disable-next-line no-console
|
|
183
|
+
console.log(`\n${loopEmoji} Loop risk: ${loopRisk}`);
|
|
184
|
+
// Recommendation
|
|
185
|
+
if (recommendation) {
|
|
186
|
+
// eslint-disable-next-line no-console
|
|
187
|
+
console.log(`\n💡 Recommendation: ${recommendation.action.toUpperCase()}`);
|
|
188
|
+
// eslint-disable-next-line no-console
|
|
189
|
+
console.log(` → ${recommendation.reason}`);
|
|
190
|
+
if (recommendation.command) {
|
|
191
|
+
// eslint-disable-next-line no-console
|
|
192
|
+
console.log(` → ${recommendation.command}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// eslint-disable-next-line no-console
|
|
196
|
+
console.log(``);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
200
|
+
// Default: One-liner Output
|
|
201
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
202
|
+
const parts = [];
|
|
203
|
+
// Profile (with preset)
|
|
204
|
+
const profileEmoji = state.profile === "concept" ? "🧠" : state.profile === "build" ? "🔨" : "🚨";
|
|
205
|
+
parts.push(`${profileEmoji} ${presetProfile}`);
|
|
206
|
+
// Last checkpoint
|
|
207
|
+
if (minutesSinceCheckpoint !== null) {
|
|
208
|
+
const cpColor = minutesSinceCheckpoint > 30 ? "🔴" : minutesSinceCheckpoint > 15 ? "🟡" : "🟢";
|
|
209
|
+
parts.push(`${cpColor} ${minutesSinceCheckpoint}m`);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
parts.push("⚪ no cp");
|
|
213
|
+
}
|
|
214
|
+
// Dirty files
|
|
215
|
+
if (dirtyFiles > 0) {
|
|
216
|
+
parts.push(`📝 ${dirtyFiles} files`);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
parts.push("✨ clean");
|
|
220
|
+
}
|
|
221
|
+
// Findings
|
|
222
|
+
if (criticalCount > 0) {
|
|
223
|
+
parts.push(`🚨 ${criticalCount} critical`);
|
|
224
|
+
}
|
|
225
|
+
else if (findingsCount > 0) {
|
|
226
|
+
parts.push(`⚠️ ${findingsCount} warn`);
|
|
227
|
+
}
|
|
228
|
+
else if (dirtyFiles > 0) {
|
|
229
|
+
parts.push("✅ ok");
|
|
230
|
+
}
|
|
231
|
+
// eslint-disable-next-line no-console
|
|
232
|
+
console.log(parts.join(" | "));
|
|
233
|
+
// Hint if overdue
|
|
234
|
+
if (minutesSinceCheckpoint !== null && minutesSinceCheckpoint > 30 && dirtyFiles > 0) {
|
|
235
|
+
// eslint-disable-next-line no-console
|
|
236
|
+
console.log("\n💡 Tip: `pulse checkpoint` to commit");
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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.registerWatchCommand = registerWatchCommand;
|
|
7
|
+
const chokidar_1 = __importDefault(require("chokidar"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const config_js_1 = require("../lib/config.js");
|
|
10
|
+
const artifacts_js_1 = require("../lib/artifacts.js");
|
|
11
|
+
const paths_js_1 = require("../lib/paths.js");
|
|
12
|
+
const git_js_1 = require("../lib/git.js");
|
|
13
|
+
const notifications_js_1 = require("../lib/notifications.js");
|
|
14
|
+
function registerWatchCommand(program) {
|
|
15
|
+
program
|
|
16
|
+
.command("watch")
|
|
17
|
+
.alias("w") // Kurzform: pulse w
|
|
18
|
+
.description("Background watcher: 30-min timer + checkpoint reminders (macOS notifications)")
|
|
19
|
+
.option("--minutes <n>", "Minutes between checkpoint reminders", "30")
|
|
20
|
+
.option("--poll-seconds <n>", "Polling interval seconds", "30")
|
|
21
|
+
.action(async (opts) => {
|
|
22
|
+
const repoRoot = await (0, paths_js_1.findRepoRoot)(process.cwd());
|
|
23
|
+
if (!repoRoot)
|
|
24
|
+
throw new Error("Not inside a git repository.");
|
|
25
|
+
const config = await (0, config_js_1.loadConfig)(repoRoot);
|
|
26
|
+
const minutes = Math.max(1, Number(opts.minutes ?? "30"));
|
|
27
|
+
const pollSeconds = Math.max(5, Number(opts.pollSeconds ?? "30"));
|
|
28
|
+
await (0, notifications_js_1.notify)(config.notifications, "Pulse watch started", `Checkpoint reminder every ${minutes} minutes. Poll: ${pollSeconds}s.`);
|
|
29
|
+
const watcher = chokidar_1.default.watch(repoRoot, {
|
|
30
|
+
ignored: [
|
|
31
|
+
/(^|[\/\\])\.git/,
|
|
32
|
+
/(^|[\/\\])node_modules/,
|
|
33
|
+
/(^|[\/\\])dist/,
|
|
34
|
+
/(^|[\/\\])build/,
|
|
35
|
+
/(^|[\/\\])coverage/,
|
|
36
|
+
/(^|[\/\\])\.pulse/,
|
|
37
|
+
],
|
|
38
|
+
ignoreInitial: true,
|
|
39
|
+
});
|
|
40
|
+
let dirtySince = null;
|
|
41
|
+
let lastReminderAt = Date.now();
|
|
42
|
+
watcher.on("all", async () => {
|
|
43
|
+
const st = await (0, git_js_1.gitStatusPorcelain)(repoRoot);
|
|
44
|
+
const dirty = st.trim().length > 0;
|
|
45
|
+
if (dirty && dirtySince == null)
|
|
46
|
+
dirtySince = Date.now();
|
|
47
|
+
if (!dirty)
|
|
48
|
+
dirtySince = null;
|
|
49
|
+
});
|
|
50
|
+
// Polling loop (git status + checkpoint age)
|
|
51
|
+
const interval = setInterval(async () => {
|
|
52
|
+
const st = await (0, git_js_1.gitStatusPorcelain)(repoRoot);
|
|
53
|
+
const dirty = st.trim().length > 0;
|
|
54
|
+
if (dirty && dirtySince == null)
|
|
55
|
+
dirtySince = Date.now();
|
|
56
|
+
if (!dirty)
|
|
57
|
+
dirtySince = null;
|
|
58
|
+
const state = await (0, artifacts_js_1.loadState)(repoRoot);
|
|
59
|
+
const lastCp = state.lastCheckpointAt ? Date.parse(state.lastCheckpointAt) : null;
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const minutesSinceLastCp = lastCp && Number.isFinite(lastCp) ? Math.floor((now - lastCp) / 60000) : null;
|
|
62
|
+
const shouldRemind = now - lastReminderAt >= minutes * 60_000;
|
|
63
|
+
if (!shouldRemind)
|
|
64
|
+
return;
|
|
65
|
+
lastReminderAt = now;
|
|
66
|
+
if (!dirty) {
|
|
67
|
+
await (0, notifications_js_1.notify)(config.notifications, "Pulse checkpoint", "Repo is clean. No checkpoint needed right now.");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const dirtyMins = dirtySince ? Math.floor((now - dirtySince) / 60000) : null;
|
|
71
|
+
await (0, notifications_js_1.notify)(config.notifications, "Pulse checkpoint now", [
|
|
72
|
+
`You have uncommitted changes.`,
|
|
73
|
+
dirtyMins != null ? `Dirty for ~${dirtyMins} min.` : "",
|
|
74
|
+
minutesSinceLastCp != null ? `Last checkpoint: ${minutesSinceLastCp} min ago.` : "No checkpoint recorded yet.",
|
|
75
|
+
`Run: pulse checkpoint`,
|
|
76
|
+
]
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.join("\n"));
|
|
79
|
+
}, pollSeconds * 1000);
|
|
80
|
+
const cleanup = async () => {
|
|
81
|
+
clearInterval(interval);
|
|
82
|
+
await watcher.close().catch(() => { });
|
|
83
|
+
};
|
|
84
|
+
process.on("SIGINT", async () => {
|
|
85
|
+
await cleanup();
|
|
86
|
+
// eslint-disable-next-line no-console
|
|
87
|
+
console.log("Pulse watch stopped.");
|
|
88
|
+
process.exit(0);
|
|
89
|
+
});
|
|
90
|
+
process.on("SIGTERM", async () => {
|
|
91
|
+
await cleanup();
|
|
92
|
+
process.exit(0);
|
|
93
|
+
});
|
|
94
|
+
// Keep process alive
|
|
95
|
+
// eslint-disable-next-line no-console
|
|
96
|
+
console.log(`Watching ${node_path_1.default.basename(repoRoot)} ... (Ctrl+C to stop)`);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function installHooks(repoRoot: string): Promise<void>;
|
|
@@ -0,0 +1,89 @@
|
|
|
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.installHooks = installHooks;
|
|
7
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
async function ensureExecutable(filePath) {
|
|
10
|
+
try {
|
|
11
|
+
await promises_1.default.chmod(filePath, 0o755);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// ignore
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function installHooks(repoRoot) {
|
|
18
|
+
const hooksDir = node_path_1.default.join(repoRoot, ".git", "hooks");
|
|
19
|
+
await promises_1.default.mkdir(hooksDir, { recursive: true });
|
|
20
|
+
const preCommit = node_path_1.default.join(hooksDir, "pre-commit");
|
|
21
|
+
const postCommit = node_path_1.default.join(hooksDir, "post-commit");
|
|
22
|
+
const prePush = node_path_1.default.join(hooksDir, "pre-push");
|
|
23
|
+
await promises_1.default.writeFile(preCommit, `#!/bin/sh
|
|
24
|
+
# Pulse mixed enforcement:
|
|
25
|
+
# - blocks CRITICAL findings (secrets, mass deletes) - exit code 2
|
|
26
|
+
# - warns on other findings but allows commit - exit code 1
|
|
27
|
+
#
|
|
28
|
+
# Bypass for this commit:
|
|
29
|
+
# PULSE_SKIP_HOOKS=1 git commit ...
|
|
30
|
+
|
|
31
|
+
if [ "$PULSE_SKIP_HOOKS" = "1" ]; then
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
pulse doctor --staged --hook pre-commit
|
|
36
|
+
EXIT_CODE=$?
|
|
37
|
+
|
|
38
|
+
# Only block on CRITICAL (exit 2), allow warnings (exit 1)
|
|
39
|
+
if [ $EXIT_CODE -eq 2 ]; then
|
|
40
|
+
echo ""
|
|
41
|
+
echo "❌ Commit blocked by PULSE (critical findings)"
|
|
42
|
+
echo " Fix issues or use: PULSE_SKIP_HOOKS=1 git commit ..."
|
|
43
|
+
exit 2
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
exit 0
|
|
47
|
+
`, "utf8");
|
|
48
|
+
await ensureExecutable(preCommit);
|
|
49
|
+
// Post-commit: Update checkpoint timestamp so timer resets on every commit
|
|
50
|
+
await promises_1.default.writeFile(postCommit, `#!/bin/sh
|
|
51
|
+
# Pulse: Track every commit as a checkpoint (reset timer)
|
|
52
|
+
# This updates .pulse/state.json with current timestamp
|
|
53
|
+
|
|
54
|
+
PULSE_DIR=".pulse"
|
|
55
|
+
STATE_FILE="$PULSE_DIR/state.json"
|
|
56
|
+
|
|
57
|
+
if [ -d "$PULSE_DIR" ]; then
|
|
58
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
|
59
|
+
|
|
60
|
+
if [ -f "$STATE_FILE" ]; then
|
|
61
|
+
# Update existing state.json
|
|
62
|
+
if command -v node >/dev/null 2>&1; then
|
|
63
|
+
node -e "
|
|
64
|
+
const fs = require('fs');
|
|
65
|
+
const state = JSON.parse(fs.readFileSync('$STATE_FILE', 'utf8'));
|
|
66
|
+
state.lastCheckpointAt = '$TIMESTAMP';
|
|
67
|
+
fs.writeFileSync('$STATE_FILE', JSON.stringify(state, null, 2));
|
|
68
|
+
" 2>/dev/null || true
|
|
69
|
+
fi
|
|
70
|
+
else
|
|
71
|
+
# Create new state.json
|
|
72
|
+
echo '{"version":1,"profile":"build","lastCheckpointAt":"'$TIMESTAMP'"}' > "$STATE_FILE"
|
|
73
|
+
fi
|
|
74
|
+
fi
|
|
75
|
+
`, "utf8");
|
|
76
|
+
await ensureExecutable(postCommit);
|
|
77
|
+
await promises_1.default.writeFile(prePush, `#!/bin/sh
|
|
78
|
+
set -e
|
|
79
|
+
|
|
80
|
+
# Pulse safeguard: never push without explicit permission.
|
|
81
|
+
# Allow push explicitly by running:
|
|
82
|
+
# PULSE_ALLOW_PUSH=1 git push ...
|
|
83
|
+
|
|
84
|
+
pulse doctor --hook pre-push
|
|
85
|
+
`, "utf8");
|
|
86
|
+
await ensureExecutable(prePush);
|
|
87
|
+
// eslint-disable-next-line no-console
|
|
88
|
+
console.log(`Installed git hooks:\n- ${preCommit}\n- ${postCommit}\n- ${prePush}`);
|
|
89
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const init_js_1 = require("./commands/init.js");
|
|
6
|
+
const profile_js_1 = require("./commands/profile.js");
|
|
7
|
+
const status_js_1 = require("./commands/status.js");
|
|
8
|
+
const start_js_1 = require("./commands/start.js");
|
|
9
|
+
const correct_js_1 = require("./commands/correct.js");
|
|
10
|
+
const review_js_1 = require("./commands/review.js");
|
|
11
|
+
const escalate_js_1 = require("./commands/escalate.js");
|
|
12
|
+
const checkpoint_js_1 = require("./commands/checkpoint.js");
|
|
13
|
+
const doctor_js_1 = require("./commands/doctor.js");
|
|
14
|
+
const learn_js_1 = require("./commands/learn.js");
|
|
15
|
+
const watch_js_1 = require("./commands/watch.js");
|
|
16
|
+
const run_js_1 = require("./commands/run.js");
|
|
17
|
+
const reset_js_1 = require("./commands/reset.js");
|
|
18
|
+
const program = new commander_1.Command();
|
|
19
|
+
program
|
|
20
|
+
.name("pulse")
|
|
21
|
+
.description("Pulse Toolkit CLI: controlled agentic development loops with guardrails, checkpoints, and escalation.")
|
|
22
|
+
.version("0.3.0");
|
|
23
|
+
(0, init_js_1.registerInitCommand)(program);
|
|
24
|
+
(0, status_js_1.registerStatusCommand)(program);
|
|
25
|
+
(0, profile_js_1.registerProfileCommand)(program);
|
|
26
|
+
(0, start_js_1.registerStartCommand)(program);
|
|
27
|
+
(0, run_js_1.registerRunCommand)(program);
|
|
28
|
+
(0, correct_js_1.registerCorrectCommand)(program);
|
|
29
|
+
(0, review_js_1.registerReviewCommand)(program);
|
|
30
|
+
(0, escalate_js_1.registerEscalateCommand)(program);
|
|
31
|
+
(0, checkpoint_js_1.registerCheckpointCommand)(program);
|
|
32
|
+
(0, doctor_js_1.registerDoctorCommand)(program);
|
|
33
|
+
(0, learn_js_1.registerLearnCommand)(program);
|
|
34
|
+
(0, watch_js_1.registerWatchCommand)(program);
|
|
35
|
+
(0, reset_js_1.registerResetCommand)(program);
|
|
36
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
37
|
+
// eslint-disable-next-line no-console
|
|
38
|
+
console.error(err?.stack || String(err));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PulseState } from "./types.js";
|
|
2
|
+
export type ArtifactKind = "pulses" | "reviews" | "escalations" | "worklogs";
|
|
3
|
+
export declare function timestampId(d?: Date): string;
|
|
4
|
+
export declare function ensurePulseDirs(repoRoot: string): Promise<void>;
|
|
5
|
+
export declare function writeArtifact(repoRoot: string, kind: ArtifactKind, name: string, content: string): Promise<string>;
|
|
6
|
+
export declare function loadState(repoRoot: string): Promise<PulseState>;
|
|
7
|
+
export declare function saveState(repoRoot: string, next: PulseState): Promise<void>;
|
|
@@ -0,0 +1,52 @@
|
|
|
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.timestampId = timestampId;
|
|
7
|
+
exports.ensurePulseDirs = ensurePulseDirs;
|
|
8
|
+
exports.writeArtifact = writeArtifact;
|
|
9
|
+
exports.loadState = loadState;
|
|
10
|
+
exports.saveState = saveState;
|
|
11
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const paths_js_1 = require("./paths.js");
|
|
14
|
+
function timestampId(d = new Date()) {
|
|
15
|
+
// 2026-01-05T12-34-56Z (filename-safe)
|
|
16
|
+
return d.toISOString().replace(/:/g, "-");
|
|
17
|
+
}
|
|
18
|
+
async function ensurePulseDirs(repoRoot) {
|
|
19
|
+
const base = (0, paths_js_1.pulseDir)(repoRoot);
|
|
20
|
+
await promises_1.default.mkdir(base, { recursive: true });
|
|
21
|
+
await promises_1.default.mkdir(node_path_1.default.join(base, "pulses"), { recursive: true });
|
|
22
|
+
await promises_1.default.mkdir(node_path_1.default.join(base, "reviews"), { recursive: true });
|
|
23
|
+
await promises_1.default.mkdir(node_path_1.default.join(base, "escalations"), { recursive: true });
|
|
24
|
+
await promises_1.default.mkdir(node_path_1.default.join(base, "worklogs"), { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
async function writeArtifact(repoRoot, kind, name, content) {
|
|
27
|
+
await ensurePulseDirs(repoRoot);
|
|
28
|
+
const p = node_path_1.default.join((0, paths_js_1.pulseDir)(repoRoot), kind, name);
|
|
29
|
+
await promises_1.default.writeFile(p, content, "utf8");
|
|
30
|
+
return p;
|
|
31
|
+
}
|
|
32
|
+
async function loadState(repoRoot) {
|
|
33
|
+
try {
|
|
34
|
+
const raw = await promises_1.default.readFile((0, paths_js_1.stateFile)(repoRoot), "utf8");
|
|
35
|
+
const parsed = JSON.parse(raw);
|
|
36
|
+
if (parsed && typeof parsed === "object") {
|
|
37
|
+
return {
|
|
38
|
+
version: 1,
|
|
39
|
+
profile: parsed.profile ?? "build",
|
|
40
|
+
lastCheckpointAt: parsed.lastCheckpointAt,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// ignore
|
|
46
|
+
}
|
|
47
|
+
return { version: 1, profile: "build" };
|
|
48
|
+
}
|
|
49
|
+
async function saveState(repoRoot, next) {
|
|
50
|
+
await ensurePulseDirs(repoRoot);
|
|
51
|
+
await promises_1.default.writeFile((0, paths_js_1.stateFile)(repoRoot), JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
52
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Briefing Library - Aggregates data for Decision Briefings
|
|
3
|
+
*/
|
|
4
|
+
import type { PulseConfig } from "./types.js";
|
|
5
|
+
import type { ScanResult, Finding } from "./scanner.js";
|
|
6
|
+
export type ScopeCheck = {
|
|
7
|
+
files: {
|
|
8
|
+
current: number;
|
|
9
|
+
max: number;
|
|
10
|
+
percent: number;
|
|
11
|
+
};
|
|
12
|
+
lines: {
|
|
13
|
+
current: number;
|
|
14
|
+
max: number;
|
|
15
|
+
percent: number;
|
|
16
|
+
};
|
|
17
|
+
deletes: {
|
|
18
|
+
current: number;
|
|
19
|
+
max: number;
|
|
20
|
+
percent: number;
|
|
21
|
+
};
|
|
22
|
+
exceeded: boolean;
|
|
23
|
+
exceededFields: string[];
|
|
24
|
+
};
|
|
25
|
+
export type RiskSummary = {
|
|
26
|
+
criticalCount: number;
|
|
27
|
+
warningCount: number;
|
|
28
|
+
findings: Finding[];
|
|
29
|
+
loopRisk: "LOW" | "MEDIUM" | "HIGH";
|
|
30
|
+
loopSignals: string[];
|
|
31
|
+
};
|
|
32
|
+
export type TimeSummary = {
|
|
33
|
+
minutesSinceCheckpoint: number | null;
|
|
34
|
+
checkpointOverdue: boolean;
|
|
35
|
+
sessionMinutes: number | null;
|
|
36
|
+
};
|
|
37
|
+
export type Recommendation = {
|
|
38
|
+
action: "approve" | "checkpoint" | "escalate" | "stop";
|
|
39
|
+
reason: string;
|
|
40
|
+
command?: string;
|
|
41
|
+
};
|
|
42
|
+
export type DecisionBriefing = {
|
|
43
|
+
preset: string | null;
|
|
44
|
+
profile: string;
|
|
45
|
+
scope: ScopeCheck;
|
|
46
|
+
risk: RiskSummary;
|
|
47
|
+
time: TimeSummary;
|
|
48
|
+
recommendation: Recommendation;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Calculate scope check against preset limits
|
|
52
|
+
*/
|
|
53
|
+
export declare function calculateScopeCheck(config: PulseConfig, stats: {
|
|
54
|
+
filesChanged: number;
|
|
55
|
+
linesAdded: number;
|
|
56
|
+
linesDeleted: number;
|
|
57
|
+
}): ScopeCheck;
|
|
58
|
+
/**
|
|
59
|
+
* Summarize risks from scan results
|
|
60
|
+
*/
|
|
61
|
+
export declare function calculateRiskSummary(scanResult: ScanResult): RiskSummary;
|
|
62
|
+
/**
|
|
63
|
+
* Calculate time-based metrics
|
|
64
|
+
*/
|
|
65
|
+
export declare function calculateTimeSummary(lastCheckpointAt: string | undefined, checkpointReminderMinutes: number): TimeSummary;
|
|
66
|
+
/**
|
|
67
|
+
* Generate recommendation based on all factors
|
|
68
|
+
*/
|
|
69
|
+
export declare function generateRecommendation(scope: ScopeCheck, risk: RiskSummary, time: TimeSummary): Recommendation;
|
|
70
|
+
/**
|
|
71
|
+
* Render progress bar
|
|
72
|
+
*/
|
|
73
|
+
export declare function renderProgressBar(percent: number, width?: number): string;
|
|
74
|
+
/**
|
|
75
|
+
* Render full Decision Briefing to terminal
|
|
76
|
+
*/
|
|
77
|
+
export declare function renderBriefing(briefing: DecisionBriefing): string;
|