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.
Files changed (64) hide show
  1. package/dist/commands/checkpoint.d.ts +2 -0
  2. package/dist/commands/checkpoint.js +129 -0
  3. package/dist/commands/correct.d.ts +2 -0
  4. package/dist/commands/correct.js +77 -0
  5. package/dist/commands/doctor.d.ts +2 -0
  6. package/dist/commands/doctor.js +183 -0
  7. package/dist/commands/escalate.d.ts +2 -0
  8. package/dist/commands/escalate.js +226 -0
  9. package/dist/commands/init.d.ts +2 -0
  10. package/dist/commands/init.js +570 -0
  11. package/dist/commands/learn.d.ts +2 -0
  12. package/dist/commands/learn.js +137 -0
  13. package/dist/commands/profile.d.ts +2 -0
  14. package/dist/commands/profile.js +39 -0
  15. package/dist/commands/reset.d.ts +2 -0
  16. package/dist/commands/reset.js +130 -0
  17. package/dist/commands/review.d.ts +2 -0
  18. package/dist/commands/review.js +129 -0
  19. package/dist/commands/run.d.ts +2 -0
  20. package/dist/commands/run.js +272 -0
  21. package/dist/commands/start.d.ts +2 -0
  22. package/dist/commands/start.js +196 -0
  23. package/dist/commands/status.d.ts +2 -0
  24. package/dist/commands/status.js +239 -0
  25. package/dist/commands/watch.d.ts +2 -0
  26. package/dist/commands/watch.js +98 -0
  27. package/dist/hooks/install.d.ts +1 -0
  28. package/dist/hooks/install.js +89 -0
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.js +40 -0
  31. package/dist/lib/artifacts.d.ts +7 -0
  32. package/dist/lib/artifacts.js +52 -0
  33. package/dist/lib/briefing.d.ts +77 -0
  34. package/dist/lib/briefing.js +231 -0
  35. package/dist/lib/clipboard.d.ts +9 -0
  36. package/dist/lib/clipboard.js +116 -0
  37. package/dist/lib/config.d.ts +14 -0
  38. package/dist/lib/config.js +167 -0
  39. package/dist/lib/context-export.d.ts +30 -0
  40. package/dist/lib/context-export.js +149 -0
  41. package/dist/lib/exec.d.ts +9 -0
  42. package/dist/lib/exec.js +23 -0
  43. package/dist/lib/git.d.ts +24 -0
  44. package/dist/lib/git.js +74 -0
  45. package/dist/lib/input.d.ts +15 -0
  46. package/dist/lib/input.js +80 -0
  47. package/dist/lib/notifications.d.ts +2 -0
  48. package/dist/lib/notifications.js +25 -0
  49. package/dist/lib/paths.d.ts +4 -0
  50. package/dist/lib/paths.js +39 -0
  51. package/dist/lib/prompts.d.ts +43 -0
  52. package/dist/lib/prompts.js +270 -0
  53. package/dist/lib/scanner.d.ts +37 -0
  54. package/dist/lib/scanner.js +413 -0
  55. package/dist/lib/types.d.ts +37 -0
  56. package/dist/lib/types.js +2 -0
  57. package/package.json +42 -0
  58. package/templates/.cursorrules +159 -0
  59. package/templates/AGENTS.md +198 -0
  60. package/templates/cursor/mcp.json +9 -0
  61. package/templates/cursor/pulse.mdc +144 -0
  62. package/templates/roles/architect.cursorrules +15 -0
  63. package/templates/roles/backend.cursorrules +12 -0
  64. 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,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerWatchCommand(program: Command): void;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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;