Skip to content

Commit d63fece

Browse files
committed
fix: promote parallel execution as primary implement strategy and add license display
Restructure spec-implement to make parallel wave execution the primary execution path (Step 2.3/2.3a) instead of an optional sub-step (2.2b). Sequential TDD becomes the fallback (2.3b) for dependent tasks. Rename spec-executor agent to spec-implementer for consistency. Update workflow-enforcement rule references and README implement phase details. Update website components: WorkflowSteps highlights parallel sub-agents, AgentRoster replaces TDD card with IMPLEMENTER/Parallel Executor, WhatsInside adds parallel execution to spec summary. Add new site components (AgentRoster, DeploymentFlow, QualifierSection, TechStack) and update ComparisonSection, HeroSection, PricingSection, Index page. Add console license display: LicenseRoutes backend endpoint with cached pilot status, LicenseBadge component with useLicense hook in Topbar, and test suites for both. Rebuild worker service and viewer bundles. Update launcher config for seat-based pricing and tool redirect tests.
1 parent 023a0d4 commit d63fece

27 files changed

Lines changed: 2527 additions & 79302 deletions

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ There are other AI coding frameworks out there. I tried them. They add complexit
6363

6464
**Pilot optimizes for output quality, not system complexity.** The rules are minimal and focused. There's no big learning curve, no project scaffolding to set up, no state files to manage. You install it, run `pilot`, and the quality guardrails are just there — hooks, TDD, type checking, formatting — enforced automatically on every edit, in every session.
6565

66-
This isn't a vibe coding tool. It's built for developers who ship to production and need code that actually works. Every rule in the system comes from daily professional use: real bugs caught, real regressions prevented, real sessions where the AI cut corners and the hooks stopped it. The rules are continuously refined based on what measurably improves output — not what looks impressive in a README.
66+
This isn't a vibe coding tool. It's built for developers who ship to production and need code that actually works. Every rule in the system comes from daily professional use: real bugs caught, real regressions prevented, real sessions where the AI cut corners and the hooks stopped it. The rules are continuously refined based on what measurably improves output.
6767

68-
The system stays fast because it stays simple. Quick mode is direct execution with zero overhead — no sub-agents, no plan files, no directory scaffolding. You describe the task and it gets done. `/spec` adds structure only when you need it: plan verification, TDD enforcement, independent code review, parallel execution. Both modes share the same quality hooks. Both modes hand off cleanly across sessions with Endless Mode. The difference is how much planning wraps the work, not whether the work is done right.
68+
The system stays fast because it stays simple. Quick mode is direct execution with zero overhead — no sub-agents, no plan files, no directory scaffolding. You describe the task and it gets done. `/spec` adds structure only when you need it: plan verification, TDD enforcement, independent code review, parallel execution. Both modes share the same quality hooks. Both modes hand off cleanly across sessions with Endless Mode.
6969

7070
---
7171

@@ -187,10 +187,12 @@ Discuss → Plan → Approve → Implement → Verify → Done
187187
<summary><b>Implement Phase</b></summary>
188188

189189
1. Creates an isolated git worktree on a dedicated branch — main branch stays clean
190-
2. Writes a failing test first (RED phase of TDD)
191-
3. Implements code to make the test pass (GREEN phase)
192-
4. Refactors while keeping tests green (REFACTOR phase)
193-
5. Quality hooks auto-lint, format, and type-check every file edit
190+
2. Analyzes task graph to detect independent tasks — groups them into parallel waves
191+
3. Spawns `spec-implementer` sub-agents for each independent task in a wave, executing TDD in parallel with fresh context windows
192+
4. Falls back to sequential execution only when tasks share files or have linear dependencies
193+
5. Each task follows strict TDD: write failing test (RED), implement to pass (GREEN), refactor (REFACTOR)
194+
6. Quality hooks auto-lint, format, and type-check every file edit
195+
7. After each wave, runs the full test suite to catch cross-task conflicts before proceeding
194196

195197
</details>
196198

console/src/services/worker-service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import { AuthRoutes } from "./worker/http/routes/AuthRoutes.js";
6565
import { PlanRoutes } from "./worker/http/routes/PlanRoutes.js";
6666
import { WorktreeRoutes } from "./worker/http/routes/WorktreeRoutes.js";
6767
import { VexorRoutes } from "./worker/http/routes/VexorRoutes.js";
68+
import { LicenseRoutes } from "./worker/http/routes/LicenseRoutes.js";
6869
import { MetricsService } from "./worker/MetricsService.js";
6970
import { startRetentionScheduler, stopRetentionScheduler } from "./worker/RetentionScheduler.js";
7071

@@ -249,6 +250,8 @@ export class WorkerService {
249250
this.vexorRoutes = new VexorRoutes(this.dbManager);
250251
this.server.registerRoutes(this.vexorRoutes);
251252

253+
this.server.registerRoutes(new LicenseRoutes());
254+
252255
startRetentionScheduler(this.dbManager);
253256
}
254257

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/**
2+
* License Routes
3+
*
4+
* Exposes license status via /api/license endpoint.
5+
* Calls `pilot status --json` and caches the result for 5 minutes.
6+
*/
7+
8+
import express, { Request, Response } from "express";
9+
import { spawnSync } from "child_process";
10+
import { existsSync } from "fs";
11+
import { homedir } from "os";
12+
import { BaseRouteHandler } from "../BaseRouteHandler.js";
13+
14+
export interface LicenseResponse {
15+
valid: boolean;
16+
tier: string | null;
17+
email: string | null;
18+
daysRemaining: number | null;
19+
isExpired: boolean;
20+
}
21+
22+
const FALLBACK_RESPONSE: LicenseResponse = {
23+
valid: false,
24+
tier: null,
25+
email: null,
26+
daysRemaining: null,
27+
isExpired: false,
28+
};
29+
30+
const CACHE_TTL_MS = 5 * 60 * 1000;
31+
32+
export class LicenseRoutes extends BaseRouteHandler {
33+
private cache: { data: LicenseResponse; expiresAt: number } | null = null;
34+
35+
setupRoutes(app: express.Application): void {
36+
app.get("/api/license", this.handleGetLicense.bind(this));
37+
}
38+
39+
private handleGetLicense = this.wrapHandler((_req: Request, res: Response): void => {
40+
res.json(this.getLicenseInfo());
41+
});
42+
43+
getLicenseInfo(): LicenseResponse {
44+
if (this.cache && Date.now() < this.cache.expiresAt) {
45+
return this.cache.data;
46+
}
47+
48+
const result = this.fetchLicenseFromCLI();
49+
this.cache = { data: result, expiresAt: Date.now() + CACHE_TTL_MS };
50+
return result;
51+
}
52+
53+
private fetchLicenseFromCLI(): LicenseResponse {
54+
const pilotPath = `${homedir()}/.pilot/bin/pilot`;
55+
56+
if (!existsSync(pilotPath)) {
57+
return { ...FALLBACK_RESPONSE };
58+
}
59+
60+
try {
61+
const proc = spawnSync(pilotPath, ["status", "--json"], {
62+
stdio: "pipe",
63+
timeout: 5000,
64+
});
65+
66+
const stdout = proc.stdout?.toString().trim();
67+
if (!stdout) {
68+
return { ...FALLBACK_RESPONSE };
69+
}
70+
71+
const data = JSON.parse(stdout);
72+
73+
if (data.success) {
74+
return {
75+
valid: true,
76+
tier: data.tier ?? null,
77+
email: data.email ?? null,
78+
daysRemaining: data.days_remaining ?? null,
79+
isExpired: false,
80+
};
81+
}
82+
83+
if (data.error === "No license found") {
84+
return { ...FALLBACK_RESPONSE };
85+
}
86+
87+
return {
88+
valid: false,
89+
tier: data.tier ?? null,
90+
email: data.email ?? null,
91+
daysRemaining: data.days_remaining ?? null,
92+
isExpired: true,
93+
};
94+
} catch {
95+
return { ...FALLBACK_RESPONSE };
96+
}
97+
}
98+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React from 'react';
2+
import { Badge, Tooltip } from './ui';
3+
import type { LicenseResponse } from '../../../services/worker/http/routes/LicenseRoutes.js';
4+
5+
interface LicenseBadgeProps {
6+
license: LicenseResponse | null;
7+
isLoading: boolean;
8+
}
9+
10+
const TIER_CONFIG: Record<string, { label: string; variant: 'primary' | 'accent' | 'warning' | 'error' }> = {
11+
solo: { label: 'Solo', variant: 'primary' },
12+
team: { label: 'Team', variant: 'accent' },
13+
trial: { label: 'Trial', variant: 'warning' },
14+
standard: { label: 'Solo', variant: 'primary' },
15+
enterprise: { label: 'Team', variant: 'accent' },
16+
};
17+
18+
function buildTooltipText(license: LicenseResponse): string {
19+
const config = TIER_CONFIG[license.tier ?? ''];
20+
const parts: string[] = [config?.label ?? license.tier ?? 'Unknown'];
21+
22+
if (license.email) {
23+
parts.push(license.email);
24+
}
25+
26+
if (license.tier === 'trial' && license.daysRemaining != null) {
27+
parts.push(`${license.daysRemaining} days remaining`);
28+
}
29+
30+
return parts.join(' · ');
31+
}
32+
33+
export function LicenseBadge({ license, isLoading }: LicenseBadgeProps) {
34+
if (isLoading || !license || !license.tier) {
35+
return null;
36+
}
37+
38+
if (license.isExpired) {
39+
return (
40+
<Tooltip text={buildTooltipText(license)} position="bottom">
41+
<Badge variant="error" size="xs">Expired</Badge>
42+
</Tooltip>
43+
);
44+
}
45+
46+
const config = TIER_CONFIG[license.tier];
47+
if (!config) {
48+
return null;
49+
}
50+
51+
const label = license.tier === 'trial' && license.daysRemaining != null
52+
? `${config.label} · ${license.daysRemaining}d left`
53+
: config.label;
54+
55+
return (
56+
<Tooltip text={buildTooltipText(license)} position="bottom">
57+
<Badge variant={config.variant} size="xs">{label}</Badge>
58+
</Tooltip>
59+
);
60+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { useState, useEffect } from 'react';
2+
import type { LicenseResponse } from '../../../services/worker/http/routes/LicenseRoutes.js';
3+
4+
interface UseLicenseResult {
5+
license: LicenseResponse | null;
6+
isLoading: boolean;
7+
}
8+
9+
export function useLicense(): UseLicenseResult {
10+
const [license, setLicense] = useState<LicenseResponse | null>(null);
11+
const [isLoading, setIsLoading] = useState(true);
12+
13+
useEffect(() => {
14+
fetch('/api/license')
15+
.then((res) => res.json())
16+
.then((data: LicenseResponse) => {
17+
setLicense(data);
18+
setIsLoading(false);
19+
})
20+
.catch(() => {
21+
setIsLoading(false);
22+
});
23+
}, []);
24+
25+
return { license, isLoading };
26+
}

console/src/ui/viewer/layouts/Topbar/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { Icon } from '../../components/ui';
2+
import { LicenseBadge } from '../../components/LicenseBadge';
3+
import { useLicense } from '../../hooks/useLicense';
24
import { TopbarActions } from './TopbarActions';
35

46
interface TopbarProps {
@@ -8,6 +10,8 @@ interface TopbarProps {
810
}
911

1012
export function Topbar({ theme, onToggleTheme, onToggleLogs }: TopbarProps) {
13+
const { license, isLoading } = useLicense();
14+
1115
return (
1216
<header className="h-14 bg-base-100 border-b border-base-300/50 flex items-center justify-between px-6 gap-4">
1317
<div className="flex items-center gap-2 text-xs text-base-content/40">
@@ -35,6 +39,8 @@ export function Topbar({ theme, onToggleTheme, onToggleLogs }: TopbarProps) {
3539
Max Ritter
3640
</a>
3741
</span>
42+
{!isLoading && license?.tier && <span className="text-base-content/20">|</span>}
43+
<LicenseBadge license={license} isLoading={isLoading} />
3844
</div>
3945
<TopbarActions theme={theme} onToggleTheme={onToggleTheme} onToggleLogs={onToggleLogs} />
4046
</header>
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* Tests for LicenseBadge component
3+
*
4+
* Tests rendering of license badge for each tier state:
5+
* solo, team, trial, expired, and null (hidden).
6+
*/
7+
import { describe, it, expect } from "bun:test";
8+
import React from "react";
9+
import { renderToStaticMarkup } from "react-dom/server";
10+
import { LicenseBadge } from "../../src/ui/viewer/components/LicenseBadge.js";
11+
import type { LicenseResponse } from "../../src/services/worker/http/routes/LicenseRoutes.js";
12+
13+
function renderBadge(license: LicenseResponse | null, isLoading = false) {
14+
return renderToStaticMarkup(
15+
React.createElement(LicenseBadge, { license, isLoading }),
16+
);
17+
}
18+
19+
describe("LicenseBadge", () => {
20+
it("should render Solo badge with primary variant", () => {
21+
const html = renderBadge({
22+
valid: true,
23+
tier: "solo",
24+
email: "user@example.com",
25+
daysRemaining: null,
26+
isExpired: false,
27+
});
28+
29+
expect(html).toContain("Solo");
30+
expect(html).toContain("badge-primary");
31+
});
32+
33+
it("should render Team badge with accent variant", () => {
34+
const html = renderBadge({
35+
valid: true,
36+
tier: "team",
37+
email: "team@example.com",
38+
daysRemaining: null,
39+
isExpired: false,
40+
});
41+
42+
expect(html).toContain("Team");
43+
expect(html).toContain("badge-accent");
44+
});
45+
46+
it("should render Trial badge with warning variant and days remaining", () => {
47+
const html = renderBadge({
48+
valid: true,
49+
tier: "trial",
50+
email: "trial@example.com",
51+
daysRemaining: 7,
52+
isExpired: false,
53+
});
54+
55+
expect(html).toContain("Trial");
56+
expect(html).toContain("7d left");
57+
expect(html).toContain("badge-warning");
58+
});
59+
60+
it("should render expired badge with error variant", () => {
61+
const html = renderBadge({
62+
valid: false,
63+
tier: "trial",
64+
email: "trial@example.com",
65+
daysRemaining: null,
66+
isExpired: true,
67+
});
68+
69+
expect(html).toContain("Expired");
70+
expect(html).toContain("badge-error");
71+
});
72+
73+
it("should render nothing when tier is null (no license or no binary)", () => {
74+
const html = renderBadge({
75+
valid: false,
76+
tier: null,
77+
email: null,
78+
daysRemaining: null,
79+
isExpired: false,
80+
});
81+
82+
expect(html).toBe("");
83+
});
84+
85+
it("should render nothing when license is null (loading state)", () => {
86+
const html = renderBadge(null);
87+
expect(html).toBe("");
88+
});
89+
90+
it("should render nothing when isLoading is true", () => {
91+
const html = renderBadge(
92+
{ valid: true, tier: "solo", email: "user@example.com", daysRemaining: null, isExpired: false },
93+
true,
94+
);
95+
expect(html).toBe("");
96+
});
97+
98+
it("should include tooltip with tier and email", () => {
99+
const html = renderBadge({
100+
valid: true,
101+
tier: "solo",
102+
email: "user@example.com",
103+
daysRemaining: null,
104+
isExpired: false,
105+
});
106+
107+
expect(html).toContain("tooltip");
108+
expect(html).toContain("Solo");
109+
expect(html).toContain("user@example.com");
110+
});
111+
112+
it("should include days remaining in tooltip for trial tier", () => {
113+
const html = renderBadge({
114+
valid: true,
115+
tier: "trial",
116+
email: "trial@example.com",
117+
daysRemaining: 5,
118+
isExpired: false,
119+
});
120+
121+
expect(html).toContain("5 days remaining");
122+
});
123+
});

0 commit comments

Comments
 (0)