Skip to content

Commit 5815d19

Browse files
committed
feat: add shipping score delta, weekly streak persistence, and AI context for performance tracking
1 parent ab0b46a commit 5815d19

11 files changed

Lines changed: 197 additions & 66 deletions

File tree

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ Five questions. AI scoring. A brief that stings when it should.
8181
- **Overall score** — averaged anchor at a glance
8282
- Riskiest assumption named explicitly
8383
- One async validation action you can do tonight
84+
- **Sunday Reminder**: Provides a helpful hint to set a recurring calendar event or cron job to close your week
8485

8586
### `loopkit track` — zero-overhead task management
8687

@@ -96,22 +97,27 @@ Five questions. AI scoring. A brief that stings when it should.
9697
- Reads your brief + completed tasks for AI context
9798
- Generates drafts for Show HN, Twitter, and Indie Hackers
9899
- Per-draft: **use**, **edit in `$EDITOR`**, **regenerate**, or **skip**
99-
- Pre-launch checklist catches what you forgot
100+
- Pre-launch checklist catches what you forgot (runs *after* draft generation to get to value faster)
101+
- AI output remains calibrated by truncating long descriptions
102+
- Auto-triggers `celebrate` confetti, but only if you actually use a draft
100103
- Ship log saved to `.loopkit/ships/YYYY-MM-DD.md`
101104

102105
### `loopkit pulse` — feedback that comes to you
103106

104107
- **Hosted Form & Widget**: Collect feedback async via your own `/pulse/[projectId]` hosted page or an embeddable JS widget on your landing page. Fully supports offline submission with automatic real-time sync.
105-
- Add responses from the CLI: `loopkit pulse --add "user said this"`
108+
- Add responses from the CLI: `loopkit pulse --add "user said this"` (automatically deduplicates)
109+
- Embeddable widget displays your actual `projectId` for easy copy-pasting
106110
- AI clusters responses into: Fix now / Validate later / Noise
107111
- Requires 5+ responses for clustering; shows raw list below threshold
108112
- Tag clusters directly to your sprint
109113

110114
### `loopkit loop` — the Sunday ritual
111115

112-
- Aggregates your week: tasks, feedback, ship logs
113-
- Tracks your consecutive **Loop Streak** (e.g. `🔥 4 consecutive weeks`)
116+
- Aggregates your week: tasks, ship logs, **pulse feedback**, and **previous weeks' context**
117+
- Tracks your consecutive **Loop Streak** (e.g. `🔥 4 consecutive weeks`) and acknowledges streak breaks
114118
- AI recommends the single highest-leverage thing for next week
119+
- Smart Unstuck Mode: kicks in if you have 0 tasks, but only after your first week
120+
- Context-aware upgrade prompts at week 4 or 8
115121
- Override rate tracked — warns if you're overriding AI > 50% of the time
116122
- Generates a build-in-public post (280-char checked)
117123
- One decision. One post. Week done.

docs/USER_GUIDE.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,9 @@ LoopKit replaces your scattered task manager with a simple `tasks.md` file:
100100

101101
- **Auto-close via git:** Write `[#1]` in your commit message → task automatically marks as done
102102
- **Shipping score:** Done tasks / Total tasks × 100. Aim for > 70%.
103+
- **Score delta:** Compare your shipping score to last week (e.g. `↑+12%` or `↓-8%`) for a daily progress anchor.
103104
- **Stale task detection:** Tasks open for 3+ days get flagged. Keep, snooze, or cut them.
105+
- **Smart Benchmarks:** Compares your performance against the network, unlocking after you reach 4 weeks of data.
104106
- **Never deleted:** Cut tasks move to `cut.md` — you might want them later.
105107

106108
**Why this matters:** Visibility creates accountability. A 60% shipping score is a data point, not a feeling.
@@ -120,12 +122,13 @@ Shipping isn't just deploying code. It's announcing it to the right people on th
120122
LoopKit asks: "What did you ship?" Then AI generates:
121123

122124
- **Hacker News:** Title + body (no hype, developer tone)
123-
- **Twitter:** 3-tweet thread (build in public style)
125+
- **Twitter:** 3-tweet thread (build in public style, with character count warnings)
124126
- **Indie Hackers:** Narrative post (story-driven)
125127

126128
You can use, edit, regenerate, or skip each draft.
129+
After reviewing the drafts, you run through a pre-launch checklist to ensure you updated your README, landing page, analytics, and feedback form.
127130

128-
**Why this matters:** Writing launch copy takes 1-2 hours. LoopKit does it in 30 seconds. The faster you launch, the faster you learn.
131+
**Why this matters:** Writing launch copy takes 1-2 hours. LoopKit does it in 30 seconds. The faster you launch, the faster you learn. By generating the drafts first, you get immediate value before doing the housekeeping tasks.
129132

130133
**Output:** Published posts + a `ship log` saved for your records.
131134

@@ -141,8 +144,8 @@ Most founders build in a vacuum. LoopKit makes feedback collection automatic.
141144

142145
**Two ways to collect:**
143146

144-
1. **Inline:** `loopkit pulse --add "User said the pricing is confusing"`
145-
2. **Shareable form:** `loopkit pulse --share` → get a URL + QR code → embed on your site
147+
1. **Inline:** `loopkit pulse --add "User said the pricing is confusing"` (automatically deduplicates)
148+
2. **Shareable form:** `loopkit pulse --share` → get a URL + QR code → embed on your site (widget outputs your actual `projectId`)
146149

147150
Once you have 5+ responses, AI clusters them into:
148151

@@ -253,7 +256,7 @@ Your shipping score is `done tasks / total tasks × 100`.
253256

254257
Consecutive weeks where you run `loopkit loop` and close your synthesis.
255258

256-
A 4-week streak means you've shipped and reflected for a month straight. This is the #1 predictor of eventual success.
259+
If you miss a week, your streak resets, but you get a gentle acknowledgment to help you start a new streak today. A 4-week streak means you've shipped and reflected for a month straight. This is the #1 predictor of eventual success.
257260

258261
### The Override
259262

@@ -274,7 +277,7 @@ But if you're overriding > 50% of the time for a month, ask yourself: _Is the sy
274277

275278
### For Consistency
276279

277-
- **Run `loopkit loop` every Sunday at the same time.** Make it a ritual.
280+
- **Run `loopkit loop` every Sunday at the same time.** Make it a ritual. LoopKit will even provide a cron reminder hint after you initialize a project.
278281
- **Set a calendar reminder.** "Sunday 9 AM — LoopKit Loop"
279282
- **Share your BIP post every week.** Public commitment = accountability.
280283

packages/cli/src/ai/prompts/init.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ MVP SCOPE:
5555
- If MVP has more than 1 core feature, tell them to cut it
5656
- The validate action must be something they can do alone tonight — no "talk to 10 customers"
5757
- If Q2 answer is a solution not a problem, flag it: "That reads as a solution. What's the pain before your product exists?"
58-
- Max 250 words total in your entire output
58+
- Be concise: each field is 1-2 sentences max. Prefer precision over length.
5959
6060
## few-shot calibration
6161

packages/cli/src/ai/prompts/loop.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,26 +64,35 @@ export function buildLoopPrompt(context: {
6464
tasksOpen: string[];
6565
shipLog?: string;
6666
pulseData?: string;
67+
previousWeeks?: Array<{ weekNumber: number; score: number; oneThing: string }>;
6768
}): string {
6869
const parts: string[] = [];
6970
parts.push(`Product: ${context.productName}`);
7071
parts.push(`Week: ${context.weekNumber}`);
7172
if (context.bet) parts.push(`Bet: ${context.bet}`);
7273
if (context.riskiestAssumption) parts.push(`Riskiest assumption: ${context.riskiestAssumption}`);
73-
74+
75+
if (context.previousWeeks && context.previousWeeks.length > 0) {
76+
parts.push(
77+
`\nRecent weeks (for accountability):\n${context.previousWeeks
78+
.map((w) => `- Week ${w.weekNumber}: ${w.score}% score · one thing was "${w.oneThing}"`)
79+
.join("\n")}`
80+
);
81+
}
82+
7483
if (context.tasksCompleted.length > 0) {
7584
parts.push(`\nTasks completed:\n${context.tasksCompleted.map((t) => `- ${t}`).join("\n")}`);
7685
} else {
7786
parts.push("\nTasks completed: None this week.");
7887
}
79-
88+
8089
if (context.tasksOpen.length > 0) {
8190
parts.push(`\nTasks still open:\n${context.tasksOpen.map((t) => `- ${t}`).join("\n")}`);
8291
}
83-
92+
8493
if (context.shipLog) parts.push(`\nShip log:\n${context.shipLog}`);
8594
if (context.pulseData) parts.push(`\nPulse feedback:\n${context.pulseData}`);
86-
95+
8796
parts.push("\nSynthesize this week and produce weekWin, oneThing, rationale, tension, bipPost, and founderNote.");
8897
return parts.join("\n");
8998
}

packages/cli/src/commands/celebrate.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,19 @@ export async function celebrateCommand(
210210
console.log(colors.dim(" Copy this to share your progress:"));
211211
console.log(box(shareText));
212212

213+
// Auto-copy to clipboard on macOS
214+
try {
215+
const { spawnSync } = await import("node:child_process");
216+
if (process.platform === "darwin") {
217+
const result = spawnSync("pbcopy", [], { input: shareText, encoding: "utf-8" });
218+
if (!result.error) {
219+
console.log(pass("Copied to clipboard — paste and share!"));
220+
}
221+
}
222+
} catch {
223+
// Silent — clipboard is a convenience, not required
224+
}
225+
213226
// ─── What's next ──────────────────────────────────────────────
214227
console.log(header("What's Next"));
215228
console.log(info("Run `loopkit loop` to synthesize your week"));

packages/cli/src/commands/init.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,13 @@ export async function initCommand(
361361
}
362362

363363
console.log(nextStep("track"));
364+
console.log(
365+
colors.dim(
366+
"\n 💡 Set a Sunday reminder to close your week:\n" +
367+
" macOS: Add a weekly calendar event for Sunday 6pm\n" +
368+
` Or cron: 0 18 * * 0 cd ${process.cwd()} && loopkit loop\n`
369+
)
370+
);
364371
p.outro(colors.muted("Brief saved. Now build against it."));
365372
}
366373

packages/cli/src/commands/loop.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,16 @@ export async function loopCommand(): Promise<void> {
9292
const tasksContent = readTasksFile(slug);
9393
const shipLog = readShipLog();
9494

95+
// ─── Build pulse context for AI ──────────────────────────────
96+
const pulseResponses = readPulseResponses();
97+
const pulseData =
98+
pulseResponses.length > 0
99+
? `${pulseResponses.length} responses. Recent: ${pulseResponses
100+
.slice(-5)
101+
.map((r, i) => `${i + 1}. "${r}"`)
102+
.join(" | ")}`
103+
: undefined;
104+
95105
// Parse tasks
96106
let tasksCompleted: string[] = [];
97107
let tasksOpen: string[] = [];
@@ -114,6 +124,24 @@ export async function loopCommand(): Promise<void> {
114124
? Math.round((tasksCompleted.length / totalTasks) * 100)
115125
: 0;
116126

127+
// ─── Build previousWeeks for AI accountability context ───────
128+
const previousLogsRaw = readLastNLoopLogs(3, slug).filter(
129+
(l) => l.weekNumber !== weekNum,
130+
);
131+
const previousWeeks = previousLogsRaw
132+
.map((l) => {
133+
const content = readLoopLog(l.weekNumber);
134+
if (!content) return null;
135+
const scoreMatch = content.match(/[Ss]hipping score:\s*(\d+)%/);
136+
const oneThingMatch = content.match(/## The One Thing\n([^\n]+)/);
137+
return {
138+
weekNumber: l.weekNumber,
139+
score: scoreMatch ? parseInt(scoreMatch[1], 10) : 0,
140+
oneThing: oneThingMatch ? oneThingMatch[1].trim() : "",
141+
};
142+
})
143+
.filter(Boolean) as Array<{ weekNumber: number; score: number; oneThing: string }>;
144+
117145
const proof = computeLoopProof({
118146
slug,
119147
weekNum,
@@ -148,6 +176,8 @@ export async function loopCommand(): Promise<void> {
148176
const currentStreak = pastStreak + 1;
149177
if (currentStreak >= 2) {
150178
console.log(` ${colors.primary.bold("Streak:")} 🔥 ${currentStreak} consecutive weeks`);
179+
} else if (pastStreak === 0 && proof.weeksActive >= 2) {
180+
console.log(warn(" Streak reset — you missed a week. Let's start a new one today."));
151181
}
152182

153183
// ─── Handle no data (first week) ─────────────────────────────
@@ -209,8 +239,8 @@ export async function loopCommand(): Promise<void> {
209239
return;
210240
}
211241

212-
// ─── Unstuck Mode: 0 tasks this week ──────────────────────────
213-
if (totalTasks === 0 && tasksCompleted.length === 0) {
242+
// ─── Unstuck Mode: 0 tasks (only triggers after week 1) ──────
243+
if (totalTasks === 0 && tasksCompleted.length === 0 && proof.weeksActive >= 2) {
214244
console.log(colors.warning("\n No tasks this week. Feeling stuck?\n"));
215245

216246
const wantUnstuck = await p.confirm({
@@ -302,6 +332,8 @@ export async function loopCommand(): Promise<void> {
302332
tasksCompleted,
303333
tasksOpen,
304334
shipLog: shipLog || undefined,
335+
pulseData,
336+
previousWeeks: previousWeeks.length > 0 ? previousWeeks : undefined,
305337
}),
306338
schema: LoopSynthesisSchema,
307339
tier: "fast",
@@ -612,11 +644,14 @@ function formatScoreDelta(delta: number): string {
612644
}
613645

614646
async function maybeShowUpgradeIntent(proof: LoopProof): Promise<void> {
615-
if (proof.weeksActive !== 1) return;
647+
if (proof.weeksActive !== 4 && proof.weeksActive !== 8) return;
616648

617-
const wantsUpgrade = await p.confirm({
618-
message: "Want the dashboard, hosted Pulse, and AI proxy when this loop starts compounding?",
619-
});
649+
const isWeek8 = proof.weeksActive === 8;
650+
const message = isWeek8
651+
? `${proof.weeksActive} weeks of data — want the dashboard and AI proxy to go deeper?`
652+
: "You're 4 weeks in — this is when the dashboard starts compounding. Want to unlock it?";
653+
654+
const wantsUpgrade = await p.confirm({ message });
620655

621656
if (p.isCancel(wantsUpgrade) || !wantsUpgrade) return;
622657

packages/cli/src/commands/pulse.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,7 @@ export async function pulseCommand(options: PulseOptions): Promise<void> {
106106

107107
console.log(
108108
colors.muted(
109-
'\n Embed widget: <script src="' +
110-
API_URL +
111-
'/api/pulse/widget?projectId=..."></script>\n',
109+
`\n Embed widget: <script src="${API_URL}/api/pulse/widget?projectId=${convexProjectId || 'YOUR_PROJECT_ID'}"></script>\n`,
112110
),
113111
);
114112
} catch (err) {
@@ -124,13 +122,25 @@ export async function pulseCommand(options: PulseOptions): Promise<void> {
124122
return;
125123
}
126124

127-
// ─── --add: Append a single response ─────────────────────────
125+
// ─── --add: Append a single response ────────────────────────────────────
128126
if (options.add !== undefined) {
129127
const text = options.add.trim();
130128
if (!text) {
131129
console.log(colors.danger("Response text cannot be empty."));
132130
process.exit(1);
133131
}
132+
133+
// Dedup: skip exact duplicates (normalized)
134+
const existing = readPulseResponses();
135+
const normalized = text.toLowerCase().replace(/\s+/g, " ");
136+
const isDuplicate = existing.some(
137+
(r) => r.toLowerCase().replace(/\s+/g, " ") === normalized
138+
);
139+
if (isDuplicate) {
140+
console.log(colors.warning("Response already exists (skipped duplicate)."));
141+
return;
142+
}
143+
134144
appendPulseResponse(text);
135145
const total = readPulseResponses().length;
136146
console.log(pass(`Response added (${total} total)`));

0 commit comments

Comments
 (0)