jettypod 4.4.115 → 4.4.118

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 (73) hide show
  1. package/.env +7 -0
  2. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +25 -9
  3. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +7 -3
  4. package/apps/dashboard/app/api/tests/run/stream/route.ts +13 -1
  5. package/apps/dashboard/app/api/usage/route.ts +17 -0
  6. package/apps/dashboard/app/connect-claude/page.tsx +24 -0
  7. package/apps/dashboard/app/install-claude/page.tsx +8 -6
  8. package/apps/dashboard/app/login/page.tsx +229 -0
  9. package/apps/dashboard/app/page.tsx +5 -3
  10. package/apps/dashboard/app/settings/page.tsx +2 -0
  11. package/apps/dashboard/app/subscribe/page.tsx +11 -0
  12. package/apps/dashboard/app/welcome/page.tsx +23 -0
  13. package/apps/dashboard/components/AppShell.tsx +51 -9
  14. package/apps/dashboard/components/CardMenu.tsx +14 -5
  15. package/apps/dashboard/components/ClaudePanel.tsx +65 -9
  16. package/apps/dashboard/components/ConnectClaudeScreen.tsx +223 -0
  17. package/apps/dashboard/components/DragContext.tsx +73 -64
  18. package/apps/dashboard/components/DraggableCard.tsx +6 -46
  19. package/apps/dashboard/components/GateCard.tsx +21 -0
  20. package/apps/dashboard/components/InstallClaudeScreen.tsx +132 -30
  21. package/apps/dashboard/components/KanbanBoard.tsx +173 -56
  22. package/apps/dashboard/components/PlaceholderCard.tsx +9 -19
  23. package/apps/dashboard/components/ProjectSwitcher.tsx +28 -0
  24. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +34 -3
  25. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +30 -2
  26. package/apps/dashboard/components/SubscribeContent.tsx +191 -0
  27. package/apps/dashboard/components/TipCard.tsx +176 -0
  28. package/apps/dashboard/components/UpgradeBanner.tsx +29 -0
  29. package/apps/dashboard/components/WelcomeScreen.tsx +14 -4
  30. package/apps/dashboard/components/settings/AccountSection.tsx +163 -0
  31. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +292 -29
  32. package/apps/dashboard/contexts/UsageContext.tsx +131 -0
  33. package/apps/dashboard/contexts/usageHelpers.js +9 -0
  34. package/apps/dashboard/electron/ipc-handlers.js +220 -114
  35. package/apps/dashboard/electron/main.js +415 -37
  36. package/apps/dashboard/electron/preload.js +23 -4
  37. package/apps/dashboard/electron/session-manager.js +141 -0
  38. package/apps/dashboard/electron-builder.config.js +3 -5
  39. package/apps/dashboard/lib/claude-process-manager.ts +6 -4
  40. package/apps/dashboard/lib/db-bridge.ts +32 -0
  41. package/apps/dashboard/lib/db.ts +159 -13
  42. package/apps/dashboard/lib/session-state-machine.ts +3 -0
  43. package/apps/dashboard/lib/session-stream-manager.ts +76 -13
  44. package/apps/dashboard/lib/tests.ts +3 -1
  45. package/apps/dashboard/next.config.js +19 -14
  46. package/apps/dashboard/package.json +3 -1
  47. package/apps/dashboard/scripts/upload-to-r2.js +89 -0
  48. package/apps/dashboard/tsconfig.tsbuildinfo +1 -0
  49. package/apps/update-server/package.json +16 -0
  50. package/apps/update-server/schema.sql +31 -0
  51. package/apps/update-server/src/index.ts +1074 -0
  52. package/apps/update-server/tsconfig.json +16 -0
  53. package/apps/update-server/wrangler.toml +35 -0
  54. package/docs/bdd-guidance.md +390 -0
  55. package/jettypod.js +5 -4
  56. package/lib/migrations/027-plan-at-creation-column.js +31 -0
  57. package/lib/migrations/028-ready-for-review-column.js +27 -0
  58. package/lib/schema.js +3 -1
  59. package/lib/seed-onboarding.js +100 -68
  60. package/lib/work-commands/index.js +43 -13
  61. package/lib/work-tracking/index.js +46 -27
  62. package/package.json +1 -1
  63. package/skills-templates/bug-mode/SKILL.md +5 -11
  64. package/skills-templates/request-routing/SKILL.md +24 -11
  65. package/skills-templates/simple-improvement/SKILL.md +35 -19
  66. package/skills-templates/stable-mode/SKILL.md +5 -6
  67. package/templates/bdd-guidance.md +139 -0
  68. package/templates/bdd-scaffolding/wait.js +18 -0
  69. package/templates/bdd-scaffolding/world.js +19 -0
  70. package/.jettypod-backup/work.db +0 -0
  71. package/apps/dashboard/app/access-code/page.tsx +0 -110
  72. package/lib/discovery-checkpoint.js +0 -123
  73. package/skills-templates/project-discovery/SKILL.md +0 -372
@@ -741,9 +741,8 @@ sqlite3 .jettypod/work.db "SELECT project_state FROM project_config WHERE id = 1
741
741
 
742
742
  **If project_state = 'internal':**
743
743
 
744
- ```bash
745
- jettypod work status <feature-id> done
746
- ```
744
+ **The merge command automatically sets the feature as ready for review.**
745
+ It will appear with accept/reject buttons on the kanban board. Do NOT call `jettypod work status <feature-id> done` — that bypasses the review gate.
747
746
 
748
747
  **Display:**
749
748
 
@@ -854,7 +853,7 @@ Before ending stable-mode skill, ensure:
854
853
  - [ ] Integration scenario still passes (feature remains reachable after error handling added)
855
854
  - [ ] Final chore merged to main
856
855
  - [ ] Project state checked (internal vs external)
857
- - [ ] **INTERNAL:** Feature marked as done
856
+ - [ ] **INTERNAL:** Feature merged (review gate set automatically by merge command)
858
857
  - [ ] **EXTERNAL:** Feature mode set to production AND production-mode skill invoked
859
858
 
860
859
  ---
@@ -878,8 +877,8 @@ jettypod work start <chore-id> # Create worktree and start chore
878
877
 
879
878
  **Set feature status/mode:**
880
879
  ```bash
881
- jettypod work status <feature-id> done # Mark feature complete (internal only)
882
- jettypod work set-mode <feature-id> production # Set feature to production mode
880
+ # ⚠️ Do NOT call `work status done` merge sets the review gate automatically
881
+ jettypod work set-mode <feature-id> production # Set feature to production mode (external only)
883
882
  ```
884
883
 
885
884
  **❌ DO NOT use these to complete chores:**
@@ -0,0 +1,139 @@
1
+ # BDD Reference for Skills
2
+
3
+ Read this before writing scenarios or step definitions. Every rule here is enforced — not advisory.
4
+
5
+ ## Scenario Formulation
6
+
7
+ **Feature files describe "what" and "why." Step definitions implement "how."**
8
+
9
+ Each scenario: one behavior, one reason to fail.
10
+
11
+ ### Rules
12
+
13
+ - **Declarative** — describe intent, not UI clicks. "When the user retries payment" not "When I click the third button."
14
+ - **Stable** — no brittle details (pixel positions, timing, CSS selectors).
15
+ - **Deterministic** — use factories with known data. Never depend on ambient/existing data.
16
+ - **No implementation leakage** — no "Given the database has..." in feature files. Use intentful language: "Given a user with an unpaid invoice."
17
+
18
+ ### Good
19
+
20
+ ```gherkin
21
+ Scenario: User can retry a failed payment
22
+ Given a user with an unpaid invoice
23
+ And the payment processor returns "insufficient_funds"
24
+ When the user retries payment with a different card
25
+ Then the invoice is marked as paid
26
+ And the user sees a receipt
27
+ ```
28
+
29
+ ### Bad
30
+
31
+ ```gherkin
32
+ Scenario: Pay invoice
33
+ Given I click the "Billing" tab
34
+ And I wait 2 seconds
35
+ And I click the third button on the page
36
+ When I type "4111111111111111" into the card field
37
+ Then I should see "Success"
38
+ ```
39
+
40
+ ## Step Definition Banlist
41
+
42
+ These are BANNED in step definitions. Not "avoid" — banned.
43
+
44
+ | Banned | Use instead |
45
+ |--------|-------------|
46
+ | `setTimeout`, `sleep`, fixed waits | `waitFor(() => condition, { timeout })` polling utility |
47
+ | Module-level `let`/`var` state | Cucumber World (`this`) for per-scenario state |
48
+ | Raw SQL (`db.run`, `db.get`) | Helper methods in `features/support/helpers/` |
49
+ | Loops and branching logic | One intentful helper call per step |
50
+ | Assertions in Given/When steps | Assertions belong in Then steps only |
51
+ | Inline selectors/click chains | Page objects or screen models |
52
+ | Setting mock state then asserting it | Tests must exercise real production code |
53
+
54
+ ### Thin Step Pattern
55
+
56
+ Steps do three things: parse parameters, call one helper, assert outcome.
57
+
58
+ ```js
59
+ // GOOD — thin step, calls helper
60
+ When('the user retries payment with a different card', async function () {
61
+ await this.billing.retryPaymentWith(this.validCard);
62
+ });
63
+
64
+ // BAD — inline implementation
65
+ When('the user retries payment with a different card', async function () {
66
+ await page.click('#billing-tab');
67
+ await page.waitForSelector('.invoice-list');
68
+ await page.click('.invoice:first-child .retry-btn');
69
+ await page.fill('#card-number', '4242424242424242');
70
+ await page.click('#submit');
71
+ await new Promise(r => setTimeout(r, 2000));
72
+ });
73
+ ```
74
+
75
+ ## Helper Layer Architecture
76
+
77
+ Step definitions must not contain domain logic, SQL, or UI interaction directly. Use this layering:
78
+
79
+ ```
80
+ Feature files (behavior — Gherkin)
81
+ └─ Step definitions (glue — thin, 1-5 lines)
82
+ └─ Helpers (features/support/helpers/)
83
+ ├─ Domain helpers (intentful operations)
84
+ ├─ API client
85
+ ├─ DB helpers (encapsulated queries)
86
+ └─ Wait utilities (polling, not sleeping)
87
+ ```
88
+
89
+ **Dependency rules:**
90
+ - Step defs call helpers. Never call drivers directly.
91
+ - Helpers call drivers (DB, API, UI).
92
+ - Feature files know nothing about implementation.
93
+
94
+ **Before writing step definitions:** Read `features/support/helpers/` for existing helpers. Reuse before creating new ones.
95
+
96
+ ## Async & External Dependencies
97
+
98
+ **Async:** Poll with timeout, never sleep.
99
+ ```js
100
+ await waitFor(() => order.status === 'COMPLETED', { timeout: 10_000 });
101
+ ```
102
+
103
+ **External services:** Stub at the boundary for most tests. Use fakes (in-memory implementations) over mocks. Mocks test call patterns ("did we call X?"); fakes test outcomes ("did the user get the result?").
104
+
105
+ **Auth flows:** Use test shortcuts (token minting, session injection). Don't make every scenario pay the login tax.
106
+
107
+ ## Test Doubles
108
+
109
+ Prefer stubs/fakes for BDD. They support behavior assertions without coupling to implementation.
110
+
111
+ Use mocks only when verifying a side effect IS the scenario's purpose (e.g., "audit event was emitted").
112
+
113
+ **BDD asserts outcomes, not internal choreography.**
114
+
115
+ ## Data Setup
116
+
117
+ Use intentful factories:
118
+ ```js
119
+ // GOOD
120
+ await this.fixtures.createUserWithUnpaidInvoice();
121
+
122
+ // BAD — implementation leakage
123
+ await db.run('INSERT INTO users...');
124
+ await db.run('INSERT INTO invoices...');
125
+ ```
126
+
127
+ Freeze time and seed randomness. Assert on meaning ("receipt exists") not raw IDs.
128
+
129
+ ## Quality Checklist
130
+
131
+ After writing tests, verify:
132
+
133
+ - [ ] Steps are 1-5 lines, each calling one helper
134
+ - [ ] No `setTimeout` or `sleep` anywhere in step definitions
135
+ - [ ] State stored on `this` (Cucumber World), not module-level variables
136
+ - [ ] No raw SQL in step definitions
137
+ - [ ] Assertions only in Then steps
138
+ - [ ] Tests exercise real production code (not asserting mock state you just set)
139
+ - [ ] Existing helpers reused, not duplicated
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Poll a condition function until it returns true, with timeout.
3
+ * Use this instead of setTimeout/sleep in step definitions.
4
+ *
5
+ * Usage:
6
+ * const { waitFor } = require('../helpers/wait');
7
+ * await waitFor(() => order.status === 'COMPLETED', { timeout: 10000 });
8
+ */
9
+ async function waitFor(conditionFn, { timeout = 5000, interval = 100 } = {}) {
10
+ const start = Date.now();
11
+ while (Date.now() - start < timeout) {
12
+ if (await conditionFn()) return;
13
+ await new Promise(r => setTimeout(r, interval));
14
+ }
15
+ throw new Error(`waitFor timed out after ${timeout}ms`);
16
+ }
17
+
18
+ module.exports = { waitFor };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Cucumber World configuration.
3
+ * Provides per-scenario state via `this` in step definitions.
4
+ * Use this instead of module-level let/var for test state.
5
+ *
6
+ * Place at: features/support/world.js
7
+ */
8
+ const { setWorldConstructor } = require('@cucumber/cucumber');
9
+
10
+ class TestWorld {
11
+ constructor() {
12
+ // Per-scenario state — automatically reset between scenarios.
13
+ // Add properties here or in step definitions via this.propName = value
14
+ }
15
+ }
16
+
17
+ setWorldConstructor(TestWorld);
18
+
19
+ module.exports = { TestWorld };
Binary file
@@ -1,110 +0,0 @@
1
- 'use client';
2
-
3
- import { useState } from 'react';
4
- import Image from 'next/image';
5
-
6
- export default function AccessCodePage() {
7
- const [code, setCode] = useState('');
8
- const [error, setError] = useState<string | null>(null);
9
- const [isSubmitting, setIsSubmitting] = useState(false);
10
-
11
- const handleSubmit = async (e: React.FormEvent) => {
12
- e.preventDefault();
13
- setError(null);
14
-
15
- if (!code.trim()) {
16
- setError('Please enter an access code.');
17
- return;
18
- }
19
-
20
- if (!window.electronAPI?.isElectron) {
21
- setError('Access code validation is only available in the desktop app.');
22
- return;
23
- }
24
-
25
- setIsSubmitting(true);
26
-
27
- try {
28
- const result = await window.electronAPI.access.validate(code.trim());
29
-
30
- if (!result.success) {
31
- setError(result.error || 'Invalid access code.');
32
- setIsSubmitting(false);
33
- return;
34
- }
35
-
36
- // Access granted - navigate to main app
37
- window.location.href = '/';
38
- } catch (err) {
39
- setError(err instanceof Error ? err.message : 'Failed to validate access code.');
40
- setIsSubmitting(false);
41
- }
42
- };
43
-
44
- return (
45
- <div className="flex flex-col items-center justify-center min-h-screen bg-white dark:bg-zinc-900 p-8">
46
- <div className="max-w-md w-full space-y-8">
47
- {/* Logo */}
48
- <div className="flex flex-col items-center space-y-4">
49
- <Image
50
- src="/jettypod_wordmark.png"
51
- alt="JettyPod"
52
- width={160}
53
- height={40}
54
- priority
55
- />
56
- <h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 text-center">
57
- Enter Access Code
58
- </h1>
59
- <p className="text-zinc-500 dark:text-zinc-400 text-center">
60
- Enter your access code to get started with JettyPod.
61
- </p>
62
- </div>
63
-
64
- {/* Error */}
65
- {error && (
66
- <div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-4 py-3 rounded-lg text-sm">
67
- {error}
68
- </div>
69
- )}
70
-
71
- {/* Form */}
72
- <form onSubmit={handleSubmit} className="pt-4 space-y-4">
73
- <input
74
- type="text"
75
- value={code}
76
- onChange={(e) => setCode(e.target.value)}
77
- placeholder="Access code"
78
- autoFocus
79
- disabled={isSubmitting}
80
- className="w-full px-4 py-3 rounded-xl border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 dark:placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-400 dark:focus:ring-zinc-500 disabled:opacity-50"
81
- data-testid="access-code-input"
82
- />
83
- <button
84
- type="submit"
85
- disabled={isSubmitting}
86
- className="w-full py-3 px-6 rounded-xl font-medium transition-all duration-200 hover:-translate-y-1 hover:scale-[1.01] active:translate-y-0 active:scale-100 disabled:opacity-50 disabled:pointer-events-none"
87
- style={{
88
- cursor: isSubmitting ? 'default' : 'pointer',
89
- background: 'linear-gradient(145deg, #ffffff 0%, #faf9f7 10%, #f0f4f4 35%, #c8d9da 55%, #819D9F 90%)',
90
- color: '#3d4d4e',
91
- boxShadow: `
92
- 0 1px 1px rgba(0, 0, 0, 0.02),
93
- 0 2px 4px rgba(0, 0, 0, 0.03),
94
- 0 6px 12px rgba(0, 0, 0, 0.05),
95
- 0 12px 24px rgba(0, 0, 0, 0.06),
96
- 0 20px 40px rgba(129, 157, 159, 0.2),
97
- 0 32px 64px rgba(129, 157, 159, 0.18),
98
- inset 0 2px 4px rgba(255, 255, 255, 1),
99
- inset 0 -2px 4px rgba(129, 157, 159, 0.05)
100
- `,
101
- }}
102
- data-testid="access-code-submit"
103
- >
104
- {isSubmitting ? 'Validating...' : 'Continue'}
105
- </button>
106
- </form>
107
- </div>
108
- </div>
109
- );
110
- }
@@ -1,123 +0,0 @@
1
- const config = require('./config');
2
-
3
- /**
4
- * Update discovery checkpoint with current progress
5
- * @param {number} step - Current discovery step (1-7)
6
- * @param {Object} data - Checkpoint data (user_journey, ux_approach, epics_created)
7
- */
8
- function updateCheckpoint(step, data = {}) {
9
- const currentConfig = config.read();
10
-
11
- // Ensure project_discovery exists
12
- if (!currentConfig.project_discovery) {
13
- currentConfig.project_discovery = config.getDefaultProjectDiscovery();
14
- }
15
-
16
- // Update checkpoint
17
- currentConfig.project_discovery.checkpoint = {
18
- step,
19
- user_journey: data.user_journey || currentConfig.project_discovery.checkpoint?.user_journey || null,
20
- ux_approach: data.ux_approach || currentConfig.project_discovery.checkpoint?.ux_approach || null,
21
- epics_created: data.epics_created !== undefined ? data.epics_created : (currentConfig.project_discovery.checkpoint?.epics_created || false)
22
- };
23
-
24
- // Update status to in_progress if not already completed
25
- if (currentConfig.project_discovery.status === 'not_started') {
26
- currentConfig.project_discovery.status = 'in_progress';
27
- currentConfig.project_discovery.started_date = new Date().toISOString();
28
- }
29
-
30
- config.write(currentConfig);
31
- }
32
-
33
- /**
34
- * Get current discovery checkpoint
35
- * @returns {Object|null} Checkpoint data or null if not started
36
- */
37
- function getCheckpoint() {
38
- const currentConfig = config.read();
39
-
40
- if (!currentConfig.project_discovery || currentConfig.project_discovery.status === 'not_started') {
41
- return null;
42
- }
43
-
44
- return currentConfig.project_discovery.checkpoint || null;
45
- }
46
-
47
- /**
48
- * Clear discovery checkpoint (when discovery is completed)
49
- */
50
- function clearCheckpoint() {
51
- const currentConfig = config.read();
52
-
53
- if (currentConfig.project_discovery) {
54
- currentConfig.project_discovery.checkpoint = config.getDefaultProjectDiscovery().checkpoint;
55
- config.write(currentConfig);
56
- }
57
- }
58
-
59
- /**
60
- * Get human-readable description of current checkpoint step
61
- * @param {number} step - Step number
62
- * @returns {string} Step description
63
- */
64
- function getStepDescription(step) {
65
- const steps = {
66
- 1: 'Starting discovery - define user journey',
67
- 2: 'User journey defined - present UX approaches',
68
- 3: 'UX approach selected - build prototypes',
69
- 4: 'Prototypes tested - break into epics',
70
- 5: 'Epics created - choose tech stack',
71
- 6: 'Tech stack chosen - record decision',
72
- 7: 'Discovery complete'
73
- };
74
-
75
- return steps[step] || 'Unknown step';
76
- }
77
-
78
- /**
79
- * Generate a formatted resume message for displaying when resuming discovery
80
- * @param {Object} checkpoint - Checkpoint object with step, user_journey, ux_approach, epics_created
81
- * @returns {string|null} Formatted resume message or null if no checkpoint
82
- */
83
- function generateResumeMessage(checkpoint) {
84
- if (!checkpoint) {
85
- return null;
86
- }
87
-
88
- const stepDesc = getStepDescription(checkpoint.step);
89
-
90
- let lines = [
91
- '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
92
- '🔄 RESUMING PROJECT DISCOVERY',
93
- '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
94
- '',
95
- `📍 Resuming from Step ${checkpoint.step}: ${stepDesc}`,
96
- ''
97
- ];
98
-
99
- if (checkpoint.user_journey) {
100
- lines.push(`📝 User Journey: "${checkpoint.user_journey}"`);
101
- }
102
- if (checkpoint.ux_approach) {
103
- lines.push(`🎨 UX Approach: "${checkpoint.ux_approach}"`);
104
- }
105
- if (checkpoint.epics_created) {
106
- lines.push(`✅ Epics: Already created`);
107
- }
108
-
109
- lines.push('');
110
- lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
111
- lines.push('');
112
- lines.push('Continuing where you left off...');
113
-
114
- return lines.join('\n');
115
- }
116
-
117
- module.exports = {
118
- updateCheckpoint,
119
- getCheckpoint,
120
- clearCheckpoint,
121
- getStepDescription,
122
- generateResumeMessage
123
- };