shipwright-cli 2.3.1 → 3.0.0

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 (162) hide show
  1. package/README.md +95 -28
  2. package/completions/_shipwright +1 -1
  3. package/completions/shipwright.bash +3 -8
  4. package/completions/shipwright.fish +1 -1
  5. package/config/defaults.json +111 -0
  6. package/config/event-schema.json +81 -0
  7. package/config/policy.json +155 -2
  8. package/config/policy.schema.json +162 -1
  9. package/dashboard/coverage/coverage-summary.json +14 -0
  10. package/dashboard/public/index.html +1 -1
  11. package/dashboard/server.ts +306 -17
  12. package/dashboard/src/components/charts/bar.test.ts +79 -0
  13. package/dashboard/src/components/charts/donut.test.ts +68 -0
  14. package/dashboard/src/components/charts/pipeline-rail.test.ts +117 -0
  15. package/dashboard/src/components/charts/sparkline.test.ts +125 -0
  16. package/dashboard/src/core/api.test.ts +309 -0
  17. package/dashboard/src/core/helpers.test.ts +301 -0
  18. package/dashboard/src/core/router.test.ts +307 -0
  19. package/dashboard/src/core/router.ts +7 -0
  20. package/dashboard/src/core/sse.test.ts +144 -0
  21. package/dashboard/src/views/metrics.test.ts +186 -0
  22. package/dashboard/src/views/overview.test.ts +173 -0
  23. package/dashboard/src/views/pipelines.test.ts +183 -0
  24. package/dashboard/src/views/team.test.ts +253 -0
  25. package/dashboard/vitest.config.ts +14 -5
  26. package/docs/TIPS.md +1 -1
  27. package/docs/patterns/README.md +1 -1
  28. package/package.json +15 -5
  29. package/scripts/adapters/docker-deploy.sh +1 -1
  30. package/scripts/adapters/tmux-adapter.sh +11 -1
  31. package/scripts/adapters/wezterm-adapter.sh +1 -1
  32. package/scripts/check-version-consistency.sh +1 -1
  33. package/scripts/lib/architecture.sh +126 -0
  34. package/scripts/lib/bootstrap.sh +75 -0
  35. package/scripts/lib/compat.sh +89 -6
  36. package/scripts/lib/config.sh +91 -0
  37. package/scripts/lib/daemon-adaptive.sh +3 -3
  38. package/scripts/lib/daemon-dispatch.sh +39 -16
  39. package/scripts/lib/daemon-health.sh +1 -1
  40. package/scripts/lib/daemon-patrol.sh +24 -12
  41. package/scripts/lib/daemon-poll.sh +37 -25
  42. package/scripts/lib/daemon-state.sh +115 -23
  43. package/scripts/lib/daemon-triage.sh +30 -8
  44. package/scripts/lib/fleet-failover.sh +63 -0
  45. package/scripts/lib/helpers.sh +30 -6
  46. package/scripts/lib/pipeline-detection.sh +2 -2
  47. package/scripts/lib/pipeline-github.sh +9 -9
  48. package/scripts/lib/pipeline-intelligence.sh +85 -35
  49. package/scripts/lib/pipeline-quality-checks.sh +16 -16
  50. package/scripts/lib/pipeline-quality.sh +1 -1
  51. package/scripts/lib/pipeline-stages.sh +242 -28
  52. package/scripts/lib/pipeline-state.sh +40 -4
  53. package/scripts/lib/test-helpers.sh +247 -0
  54. package/scripts/postinstall.mjs +3 -11
  55. package/scripts/sw +10 -4
  56. package/scripts/sw-activity.sh +1 -11
  57. package/scripts/sw-adaptive.sh +109 -85
  58. package/scripts/sw-adversarial.sh +4 -14
  59. package/scripts/sw-architecture-enforcer.sh +1 -11
  60. package/scripts/sw-auth.sh +8 -17
  61. package/scripts/sw-autonomous.sh +111 -49
  62. package/scripts/sw-changelog.sh +1 -11
  63. package/scripts/sw-checkpoint.sh +144 -20
  64. package/scripts/sw-ci.sh +2 -12
  65. package/scripts/sw-cleanup.sh +13 -17
  66. package/scripts/sw-code-review.sh +16 -36
  67. package/scripts/sw-connect.sh +5 -12
  68. package/scripts/sw-context.sh +9 -26
  69. package/scripts/sw-cost.sh +6 -16
  70. package/scripts/sw-daemon.sh +75 -70
  71. package/scripts/sw-dashboard.sh +57 -17
  72. package/scripts/sw-db.sh +506 -15
  73. package/scripts/sw-decompose.sh +1 -11
  74. package/scripts/sw-deps.sh +15 -25
  75. package/scripts/sw-developer-simulation.sh +1 -11
  76. package/scripts/sw-discovery.sh +112 -30
  77. package/scripts/sw-doc-fleet.sh +7 -17
  78. package/scripts/sw-docs-agent.sh +6 -16
  79. package/scripts/sw-docs.sh +4 -12
  80. package/scripts/sw-doctor.sh +134 -43
  81. package/scripts/sw-dora.sh +11 -19
  82. package/scripts/sw-durable.sh +35 -52
  83. package/scripts/sw-e2e-orchestrator.sh +11 -27
  84. package/scripts/sw-eventbus.sh +115 -115
  85. package/scripts/sw-evidence.sh +748 -0
  86. package/scripts/sw-feedback.sh +3 -13
  87. package/scripts/sw-fix.sh +2 -20
  88. package/scripts/sw-fleet-discover.sh +1 -11
  89. package/scripts/sw-fleet-viz.sh +10 -18
  90. package/scripts/sw-fleet.sh +13 -17
  91. package/scripts/sw-github-app.sh +6 -16
  92. package/scripts/sw-github-checks.sh +1 -11
  93. package/scripts/sw-github-deploy.sh +1 -11
  94. package/scripts/sw-github-graphql.sh +2 -12
  95. package/scripts/sw-guild.sh +1 -11
  96. package/scripts/sw-heartbeat.sh +49 -12
  97. package/scripts/sw-hygiene.sh +45 -43
  98. package/scripts/sw-incident.sh +284 -67
  99. package/scripts/sw-init.sh +35 -37
  100. package/scripts/sw-instrument.sh +1 -11
  101. package/scripts/sw-intelligence.sh +362 -51
  102. package/scripts/sw-jira.sh +5 -14
  103. package/scripts/sw-launchd.sh +2 -12
  104. package/scripts/sw-linear.sh +8 -17
  105. package/scripts/sw-logs.sh +4 -12
  106. package/scripts/sw-loop.sh +641 -90
  107. package/scripts/sw-memory.sh +243 -17
  108. package/scripts/sw-mission-control.sh +2 -12
  109. package/scripts/sw-model-router.sh +73 -34
  110. package/scripts/sw-otel.sh +11 -21
  111. package/scripts/sw-oversight.sh +1 -11
  112. package/scripts/sw-patrol-meta.sh +5 -11
  113. package/scripts/sw-pipeline-composer.sh +7 -17
  114. package/scripts/sw-pipeline-vitals.sh +1 -11
  115. package/scripts/sw-pipeline.sh +478 -122
  116. package/scripts/sw-pm.sh +2 -12
  117. package/scripts/sw-pr-lifecycle.sh +203 -29
  118. package/scripts/sw-predictive.sh +16 -22
  119. package/scripts/sw-prep.sh +6 -16
  120. package/scripts/sw-ps.sh +1 -11
  121. package/scripts/sw-public-dashboard.sh +2 -12
  122. package/scripts/sw-quality.sh +77 -10
  123. package/scripts/sw-reaper.sh +1 -11
  124. package/scripts/sw-recruit.sh +15 -25
  125. package/scripts/sw-regression.sh +11 -21
  126. package/scripts/sw-release-manager.sh +19 -28
  127. package/scripts/sw-release.sh +8 -16
  128. package/scripts/sw-remote.sh +1 -11
  129. package/scripts/sw-replay.sh +48 -44
  130. package/scripts/sw-retro.sh +70 -92
  131. package/scripts/sw-review-rerun.sh +220 -0
  132. package/scripts/sw-scale.sh +109 -32
  133. package/scripts/sw-security-audit.sh +12 -22
  134. package/scripts/sw-self-optimize.sh +239 -23
  135. package/scripts/sw-session.sh +3 -13
  136. package/scripts/sw-setup.sh +8 -18
  137. package/scripts/sw-standup.sh +5 -15
  138. package/scripts/sw-status.sh +32 -23
  139. package/scripts/sw-strategic.sh +129 -13
  140. package/scripts/sw-stream.sh +1 -11
  141. package/scripts/sw-swarm.sh +76 -36
  142. package/scripts/sw-team-stages.sh +10 -20
  143. package/scripts/sw-templates.sh +4 -14
  144. package/scripts/sw-testgen.sh +3 -13
  145. package/scripts/sw-tmux-pipeline.sh +1 -19
  146. package/scripts/sw-tmux-role-color.sh +0 -10
  147. package/scripts/sw-tmux-status.sh +3 -11
  148. package/scripts/sw-tmux.sh +2 -20
  149. package/scripts/sw-trace.sh +1 -19
  150. package/scripts/sw-tracker-github.sh +0 -10
  151. package/scripts/sw-tracker-jira.sh +1 -11
  152. package/scripts/sw-tracker-linear.sh +1 -11
  153. package/scripts/sw-tracker.sh +7 -24
  154. package/scripts/sw-triage.sh +24 -34
  155. package/scripts/sw-upgrade.sh +5 -23
  156. package/scripts/sw-ux.sh +1 -19
  157. package/scripts/sw-webhook.sh +18 -32
  158. package/scripts/sw-widgets.sh +3 -21
  159. package/scripts/sw-worktree.sh +11 -27
  160. package/scripts/update-homebrew-sha.sh +67 -0
  161. package/templates/pipelines/tdd.json +72 -0
  162. package/scripts/sw-pipeline.sh.mock +0 -7
@@ -0,0 +1,173 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { store } from "../core/state";
3
+ import type { FleetState } from "../types/api";
4
+
5
+ vi.mock("../core/api", () => ({
6
+ fetchQueueDetailed: vi.fn().mockResolvedValue({ items: [] }),
7
+ }));
8
+
9
+ vi.mock("./pipelines", () => ({
10
+ fetchPipelineDetail: vi.fn(),
11
+ }));
12
+
13
+ vi.mock("../components/header", () => ({
14
+ renderCostTicker: vi.fn(),
15
+ }));
16
+
17
+ function createOverviewDOM(): void {
18
+ const ids = [
19
+ "stat-status",
20
+ "status-dot",
21
+ "stat-active",
22
+ "stat-active-bar",
23
+ "stat-queue",
24
+ "stat-queue-sub",
25
+ "stat-completed",
26
+ "stat-failed-sub",
27
+ "active-pipelines",
28
+ "queue-list",
29
+ "activity-feed",
30
+ "res-cpu-bar",
31
+ "res-cpu-info",
32
+ "res-mem-bar",
33
+ "res-mem-info",
34
+ "res-budget-bar",
35
+ "res-budget-info",
36
+ "resource-constraint",
37
+ "machines-section",
38
+ "machines-grid",
39
+ ];
40
+ for (const id of ids) {
41
+ if (!document.getElementById(id)) {
42
+ const el = document.createElement("div");
43
+ el.id = id;
44
+ document.body.appendChild(el);
45
+ }
46
+ }
47
+ }
48
+
49
+ function cleanupOverviewDOM(): void {
50
+ const ids = [
51
+ "stat-status",
52
+ "status-dot",
53
+ "stat-active",
54
+ "stat-active-bar",
55
+ "stat-queue",
56
+ "stat-queue-sub",
57
+ "stat-completed",
58
+ "stat-failed-sub",
59
+ "active-pipelines",
60
+ "queue-list",
61
+ "activity-feed",
62
+ "res-cpu-bar",
63
+ "res-cpu-info",
64
+ "res-mem-bar",
65
+ "res-mem-info",
66
+ "res-budget-bar",
67
+ "res-budget-info",
68
+ "resource-constraint",
69
+ "machines-section",
70
+ "machines-grid",
71
+ ];
72
+ ids.forEach((id) => document.getElementById(id)?.remove());
73
+ }
74
+
75
+ function emptyFleetState(): FleetState {
76
+ return {
77
+ timestamp: new Date().toISOString(),
78
+ daemon: {
79
+ running: false,
80
+ pid: null,
81
+ uptime_s: 0,
82
+ maxParallel: 0,
83
+ pollInterval: 5,
84
+ },
85
+ pipelines: [],
86
+ queue: [],
87
+ events: [],
88
+ scale: {},
89
+ metrics: {},
90
+ agents: [],
91
+ machines: [],
92
+ cost: { today_spent: 0, daily_budget: 0, pct_used: 0 },
93
+ dora: {} as any,
94
+ };
95
+ }
96
+
97
+ describe("OverviewView", () => {
98
+ beforeEach(() => {
99
+ store.set("firstRender", false);
100
+ createOverviewDOM();
101
+ });
102
+
103
+ afterEach(() => {
104
+ cleanupOverviewDOM();
105
+ vi.clearAllMocks();
106
+ });
107
+
108
+ it("renders without crashing when given empty data", async () => {
109
+ const { overviewView } = await import("./overview");
110
+ const data = emptyFleetState();
111
+ expect(() => overviewView.render(data)).not.toThrow();
112
+ });
113
+
114
+ it("renders pipeline summary section with empty pipelines", async () => {
115
+ const { overviewView } = await import("./overview");
116
+ const data = emptyFleetState();
117
+ overviewView.render(data);
118
+ const container = document.getElementById("active-pipelines");
119
+ expect(container).toBeTruthy();
120
+ expect(container!.innerHTML).toContain("No active pipelines");
121
+ });
122
+
123
+ it("renders pipeline cards when pipelines exist", async () => {
124
+ const { overviewView } = await import("./overview");
125
+ const data = emptyFleetState();
126
+ data.pipelines = [
127
+ {
128
+ issue: 42,
129
+ title: "Fix bug",
130
+ stage: "code",
131
+ stagesDone: ["plan"],
132
+ elapsed_s: 120,
133
+ iteration: 2,
134
+ maxIterations: 20,
135
+ },
136
+ ];
137
+ overviewView.render(data);
138
+ const container = document.getElementById("active-pipelines");
139
+ expect(container).toBeTruthy();
140
+ expect(container!.innerHTML).toContain("#42");
141
+ expect(container!.innerHTML).toContain("Fix bug");
142
+ expect(container!.innerHTML).toContain("pipeline-card");
143
+ });
144
+
145
+ it("handles null/undefined state gracefully", async () => {
146
+ const { overviewView } = await import("./overview");
147
+ const data = emptyFleetState();
148
+ (data as any).pipelines = null;
149
+ (data as any).queue = undefined;
150
+ (data as any).events = undefined;
151
+ expect(() => overviewView.render(data)).not.toThrow();
152
+ const pipelinesEl = document.getElementById("active-pipelines");
153
+ expect(pipelinesEl!.innerHTML).toContain("No active pipelines");
154
+ });
155
+
156
+ it("renders queue empty state", async () => {
157
+ const { overviewView } = await import("./overview");
158
+ const data = emptyFleetState();
159
+ overviewView.render(data);
160
+ const queueEl = document.getElementById("queue-list");
161
+ expect(queueEl).toBeTruthy();
162
+ expect(queueEl!.innerHTML).toContain("Queue clear");
163
+ });
164
+
165
+ it("renders activity empty state", async () => {
166
+ const { overviewView } = await import("./overview");
167
+ const data = emptyFleetState();
168
+ overviewView.render(data);
169
+ const activityEl = document.getElementById("activity-feed");
170
+ expect(activityEl).toBeTruthy();
171
+ expect(activityEl!.innerHTML).toContain("Awaiting events");
172
+ });
173
+ });
@@ -0,0 +1,183 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { store } from "../core/state";
3
+ import type { FleetState } from "../types/api";
4
+
5
+ vi.mock("../core/api", () => ({
6
+ fetchPipelineDetail: vi.fn().mockResolvedValue({}),
7
+ fetchPipelineReasoning: vi.fn().mockResolvedValue({ reasoning: [] }),
8
+ fetchPipelineFailures: vi.fn().mockResolvedValue({ failures: [] }),
9
+ fetchArtifact: vi.fn().mockResolvedValue({ content: "" }),
10
+ fetchGitHubStatus: vi.fn().mockResolvedValue({ configured: false }),
11
+ fetchPipelineQuality: vi.fn().mockResolvedValue({ results: [] }),
12
+ fetchApprovalGates: vi.fn().mockResolvedValue({ enabled: false }),
13
+ fetchLogs: vi.fn().mockResolvedValue({ content: "" }),
14
+ }));
15
+
16
+ vi.mock("../components/modal", () => ({
17
+ updateBulkToolbar: vi.fn(),
18
+ }));
19
+
20
+ function createPipelinesDOM(): void {
21
+ const tbody = document.createElement("tbody");
22
+ tbody.id = "pipeline-table-body";
23
+ document.body.appendChild(tbody);
24
+
25
+ const filters = document.createElement("div");
26
+ filters.id = "pipeline-filters";
27
+ const chipAll = document.createElement("span");
28
+ chipAll.className = "filter-chip active";
29
+ chipAll.setAttribute("data-filter", "all");
30
+ filters.appendChild(chipAll);
31
+ document.body.appendChild(filters);
32
+
33
+ const closeBtn = document.createElement("button");
34
+ closeBtn.id = "detail-panel-close";
35
+ document.body.appendChild(closeBtn);
36
+
37
+ const selectAll = document.createElement("input");
38
+ selectAll.id = "pipeline-select-all";
39
+ selectAll.type = "checkbox";
40
+ document.body.appendChild(selectAll);
41
+
42
+ const panel = document.createElement("div");
43
+ panel.id = "pipeline-detail-panel";
44
+ const title = document.createElement("div");
45
+ title.id = "detail-panel-title";
46
+ const body = document.createElement("div");
47
+ body.id = "detail-panel-body";
48
+ panel.appendChild(title);
49
+ panel.appendChild(body);
50
+ document.body.appendChild(panel);
51
+ }
52
+
53
+ function cleanupPipelinesDOM(): void {
54
+ [
55
+ "pipeline-table-body",
56
+ "pipeline-filters",
57
+ "detail-panel-close",
58
+ "pipeline-select-all",
59
+ "pipeline-detail-panel",
60
+ ].forEach((id) => document.getElementById(id)?.remove());
61
+ }
62
+
63
+ function emptyFleetState(): FleetState {
64
+ return {
65
+ timestamp: new Date().toISOString(),
66
+ daemon: {
67
+ running: false,
68
+ pid: null,
69
+ uptime_s: 0,
70
+ maxParallel: 0,
71
+ pollInterval: 5,
72
+ },
73
+ pipelines: [],
74
+ queue: [],
75
+ events: [],
76
+ scale: {},
77
+ metrics: {},
78
+ agents: [],
79
+ machines: [],
80
+ cost: { today_spent: 0, daily_budget: 0, pct_used: 0 },
81
+ dora: {} as any,
82
+ };
83
+ }
84
+
85
+ describe("PipelinesView", () => {
86
+ beforeEach(() => {
87
+ store.set("pipelineFilter", "all");
88
+ store.set("selectedPipelineIssue", null);
89
+ store.set("selectedIssues", {});
90
+ store.set("fleetState", emptyFleetState());
91
+ createPipelinesDOM();
92
+ });
93
+
94
+ afterEach(() => {
95
+ cleanupPipelinesDOM();
96
+ vi.clearAllMocks();
97
+ });
98
+
99
+ it("renders pipeline list from mock data", async () => {
100
+ const { pipelinesView } = await import("./pipelines");
101
+ pipelinesView.init();
102
+ const data = emptyFleetState();
103
+ data.pipelines = [
104
+ {
105
+ issue: 100,
106
+ title: "Add feature X",
107
+ stage: "code",
108
+ stagesDone: ["plan", "design"],
109
+ elapsed_s: 300,
110
+ iteration: 3,
111
+ maxIterations: 20,
112
+ },
113
+ ];
114
+ pipelinesView.render(data);
115
+ const tbody = document.getElementById("pipeline-table-body");
116
+ expect(tbody).toBeTruthy();
117
+ expect(tbody!.innerHTML).toContain("#100");
118
+ expect(tbody!.innerHTML).toContain("Add feature X");
119
+ expect(tbody!.innerHTML).toContain("ACTIVE");
120
+ });
121
+
122
+ it("renders empty state when no pipelines", async () => {
123
+ const { pipelinesView } = await import("./pipelines");
124
+ pipelinesView.init();
125
+ const data = emptyFleetState();
126
+ pipelinesView.render(data);
127
+ const tbody = document.getElementById("pipeline-table-body");
128
+ expect(tbody).toBeTruthy();
129
+ expect(tbody!.innerHTML).toContain("No pipelines match filter");
130
+ });
131
+
132
+ it("handles various pipeline statuses from events", async () => {
133
+ const { pipelinesView } = await import("./pipelines");
134
+ pipelinesView.init();
135
+ const data = emptyFleetState();
136
+ data.pipelines = [];
137
+ data.events = [
138
+ {
139
+ type: "pipeline.completed",
140
+ issue: 50,
141
+ issueTitle: "Done",
142
+ duration_s: 600,
143
+ },
144
+ {
145
+ type: "pipeline.failed",
146
+ issue: 51,
147
+ issueTitle: "Failed",
148
+ duration_s: 120,
149
+ },
150
+ ];
151
+ pipelinesView.render(data);
152
+ const tbody = document.getElementById("pipeline-table-body");
153
+ expect(tbody!.innerHTML).toContain("#50");
154
+ expect(tbody!.innerHTML).toContain("#51");
155
+ expect(tbody!.innerHTML).toContain("COMPLETED");
156
+ expect(tbody!.innerHTML).toContain("FAILED");
157
+ });
158
+
159
+ it("filters by status when pipelineFilter is set", async () => {
160
+ const { pipelinesView } = await import("./pipelines");
161
+ store.set("pipelineFilter", "active");
162
+ pipelinesView.init();
163
+ const data = emptyFleetState();
164
+ data.pipelines = [
165
+ {
166
+ issue: 1,
167
+ title: "Active",
168
+ stage: "code",
169
+ stagesDone: [],
170
+ elapsed_s: 60,
171
+ iteration: 1,
172
+ maxIterations: 20,
173
+ },
174
+ ];
175
+ data.events = [
176
+ { type: "pipeline.completed", issue: 2, issueTitle: "Done" },
177
+ ];
178
+ pipelinesView.render(data);
179
+ const tbody = document.getElementById("pipeline-table-body");
180
+ expect(tbody!.innerHTML).toContain("#1");
181
+ expect(tbody!.innerHTML).not.toContain("#2");
182
+ });
183
+ });
@@ -0,0 +1,253 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { store } from "../core/state";
3
+ import type { FleetState, TeamData } from "../types/api";
4
+
5
+ vi.mock("../core/api", () => ({
6
+ fetchTeam: vi
7
+ .fn()
8
+ .mockResolvedValue({
9
+ developers: [],
10
+ total_online: 0,
11
+ total_active_pipelines: 0,
12
+ total_queued: 0,
13
+ }),
14
+ fetchTeamActivity: vi.fn().mockResolvedValue([]),
15
+ createTeamInvite: vi
16
+ .fn()
17
+ .mockResolvedValue({ url: "https://invite.example.com/abc" }),
18
+ fetchLinearStatus: vi.fn().mockResolvedValue({ connected: false }),
19
+ fetchDbHealth: vi.fn().mockResolvedValue({ ok: true }),
20
+ fetchDbEvents: vi.fn().mockResolvedValue([]),
21
+ fetchDbJobs: vi.fn().mockResolvedValue([]),
22
+ fetchDbHeartbeats: vi.fn().mockResolvedValue([]),
23
+ fetchDbCostsToday: vi.fn().mockResolvedValue([]),
24
+ claimIssue: vi.fn().mockResolvedValue({ approved: true }),
25
+ releaseIssue: vi.fn().mockResolvedValue({}),
26
+ }));
27
+
28
+ function createTeamDOM(): void {
29
+ const grid = document.createElement("div");
30
+ grid.id = "team-grid";
31
+ document.body.appendChild(grid);
32
+
33
+ const statOnline = document.createElement("span");
34
+ statOnline.id = "team-stat-online";
35
+ document.body.appendChild(statOnline);
36
+
37
+ const statPipelines = document.createElement("span");
38
+ statPipelines.id = "team-stat-pipelines";
39
+ document.body.appendChild(statPipelines);
40
+
41
+ const statQueued = document.createElement("span");
42
+ statQueued.id = "team-stat-queued";
43
+ document.body.appendChild(statQueued);
44
+
45
+ const activity = document.createElement("div");
46
+ activity.id = "team-activity";
47
+ document.body.appendChild(activity);
48
+
49
+ const inviteResult = document.createElement("div");
50
+ inviteResult.id = "team-invite-result";
51
+ inviteResult.style.display = "none";
52
+ document.body.appendChild(inviteResult);
53
+
54
+ const btnInvite = document.createElement("button");
55
+ btnInvite.id = "btn-create-invite";
56
+ document.body.appendChild(btnInvite);
57
+
58
+ const integrations = document.createElement("div");
59
+ integrations.id = "integrations-status";
60
+ document.body.appendChild(integrations);
61
+
62
+ const adminOutput = document.createElement("div");
63
+ adminOutput.id = "admin-debug-output";
64
+ document.body.appendChild(adminOutput);
65
+
66
+ const claimResult = document.createElement("div");
67
+ claimResult.id = "claim-result";
68
+ document.body.appendChild(claimResult);
69
+
70
+ const claimIssue = document.createElement("input");
71
+ claimIssue.id = "claim-issue";
72
+ document.body.appendChild(claimIssue);
73
+
74
+ const claimMachine = document.createElement("input");
75
+ claimMachine.id = "claim-machine";
76
+ document.body.appendChild(claimMachine);
77
+
78
+ const btnClaim = document.createElement("button");
79
+ btnClaim.id = "btn-claim";
80
+ document.body.appendChild(btnClaim);
81
+
82
+ const btnRelease = document.createElement("button");
83
+ btnRelease.id = "btn-release";
84
+ document.body.appendChild(btnRelease);
85
+
86
+ [
87
+ "btn-db-health",
88
+ "btn-db-events",
89
+ "btn-db-jobs",
90
+ "btn-db-heartbeats",
91
+ "btn-db-costs",
92
+ ].forEach((id) => {
93
+ const btn = document.createElement("button");
94
+ btn.id = id;
95
+ document.body.appendChild(btn);
96
+ });
97
+ }
98
+
99
+ function cleanupTeamDOM(): void {
100
+ const ids = [
101
+ "team-grid",
102
+ "team-stat-online",
103
+ "team-stat-pipelines",
104
+ "team-stat-queued",
105
+ "team-activity",
106
+ "team-invite-result",
107
+ "btn-create-invite",
108
+ "integrations-status",
109
+ "admin-debug-output",
110
+ "claim-result",
111
+ "claim-issue",
112
+ "claim-machine",
113
+ "btn-claim",
114
+ "btn-release",
115
+ "btn-db-health",
116
+ "btn-db-events",
117
+ "btn-db-jobs",
118
+ "btn-db-heartbeats",
119
+ "btn-db-costs",
120
+ ];
121
+ ids.forEach((id) => document.getElementById(id)?.remove());
122
+ }
123
+
124
+ function emptyFleetState(): FleetState {
125
+ return {
126
+ timestamp: new Date().toISOString(),
127
+ daemon: {
128
+ running: false,
129
+ pid: null,
130
+ uptime_s: 0,
131
+ maxParallel: 0,
132
+ pollInterval: 5,
133
+ },
134
+ pipelines: [],
135
+ queue: [],
136
+ events: [],
137
+ scale: {},
138
+ metrics: {},
139
+ agents: [],
140
+ machines: [],
141
+ cost: { today_spent: 0, daily_budget: 0, pct_used: 0 },
142
+ dora: {} as any,
143
+ };
144
+ }
145
+
146
+ describe("TeamView", () => {
147
+ beforeEach(() => {
148
+ store.set("teamCache", null);
149
+ createTeamDOM();
150
+ });
151
+
152
+ afterEach(() => {
153
+ cleanupTeamDOM();
154
+ vi.clearAllMocks();
155
+ });
156
+
157
+ it("renders team members when data provided", async () => {
158
+ const { teamView } = await import("./team");
159
+ const teamData: TeamData = {
160
+ total_online: 2,
161
+ total_active_pipelines: 1,
162
+ total_queued: 0,
163
+ developers: [
164
+ {
165
+ developer_id: "alice",
166
+ machine_name: "macbook-pro",
167
+ daemon_running: true,
168
+ active_jobs: [{ issue: 42, title: "Fix bug", stage: "code" }],
169
+ queued: [],
170
+ },
171
+ ],
172
+ };
173
+ const data = emptyFleetState();
174
+ data.team = teamData;
175
+ teamView.render(data);
176
+ const grid = document.getElementById("team-grid");
177
+ expect(grid).toBeTruthy();
178
+ expect(grid!.innerHTML).toContain("alice");
179
+ expect(grid!.innerHTML).toContain("macbook-pro");
180
+ expect(grid!.innerHTML).toContain("#42");
181
+ expect(grid!.innerHTML).toContain("presence-dot");
182
+ });
183
+
184
+ it("handles empty team", async () => {
185
+ const { teamView } = await import("./team");
186
+ const data = emptyFleetState();
187
+ data.team = {
188
+ developers: [],
189
+ total_online: 0,
190
+ total_active_pipelines: 0,
191
+ total_queued: 0,
192
+ };
193
+ teamView.render(data);
194
+ const grid = document.getElementById("team-grid");
195
+ expect(grid).toBeTruthy();
196
+ expect(grid!.innerHTML).toContain("No developers connected");
197
+ });
198
+
199
+ it("shows activity indicators when team has active jobs", async () => {
200
+ const { teamView } = await import("./team");
201
+ const teamData: TeamData = {
202
+ developers: [
203
+ {
204
+ developer_id: "bob",
205
+ machine_name: "dev-machine",
206
+ daemon_running: true,
207
+ active_jobs: [
208
+ { issue: 10, stage: "plan" },
209
+ { issue: 20, stage: "code" },
210
+ ],
211
+ queued: [30],
212
+ },
213
+ ],
214
+ };
215
+ const data = emptyFleetState();
216
+ data.team = teamData;
217
+ teamView.render(data);
218
+ const grid = document.getElementById("team-grid");
219
+ expect(grid!.innerHTML).toContain("team-card-pipeline-item");
220
+ expect(grid!.innerHTML).toContain("#10");
221
+ expect(grid!.innerHTML).toContain("#20");
222
+ });
223
+
224
+ it("uses teamCache when data.team is absent", async () => {
225
+ const { teamView } = await import("./team");
226
+ const cachedTeam: TeamData = {
227
+ developers: [
228
+ {
229
+ developer_id: "cached",
230
+ machine_name: "cache-machine",
231
+ daemon_running: false,
232
+ active_jobs: [],
233
+ queued: [],
234
+ },
235
+ ],
236
+ total_online: 1,
237
+ total_active_pipelines: 0,
238
+ total_queued: 0,
239
+ };
240
+ store.set("teamCache", cachedTeam);
241
+ const data = emptyFleetState();
242
+ data.team = undefined;
243
+ teamView.render(data);
244
+ const grid = document.getElementById("team-grid");
245
+ expect(grid!.innerHTML).toContain("cached");
246
+ });
247
+
248
+ it("init and destroy do not throw", async () => {
249
+ const { teamView } = await import("./team");
250
+ expect(() => teamView.init()).not.toThrow();
251
+ expect(() => teamView.destroy()).not.toThrow();
252
+ });
253
+ });
@@ -10,12 +10,21 @@ export default defineConfig({
10
10
  provider: "v8",
11
11
  reporter: ["text", "json-summary"],
12
12
  include: ["src/**/*.ts"],
13
- exclude: ["src/**/*.test.ts", "src/types/**"],
13
+ exclude: [
14
+ "src/**/*.test.ts",
15
+ "src/types/**",
16
+ "src/views/**",
17
+ "src/canvas/**",
18
+ "src/components/header.ts",
19
+ "src/components/modal.ts",
20
+ "src/components/terminal.ts",
21
+ "src/main.ts",
22
+ ],
14
23
  thresholds: {
15
- statements: 60,
16
- branches: 50,
17
- functions: 60,
18
- lines: 60,
24
+ statements: 70,
25
+ branches: 60,
26
+ functions: 70,
27
+ lines: 70,
19
28
  },
20
29
  },
21
30
  },
package/docs/TIPS.md CHANGED
@@ -131,7 +131,7 @@ This compacts the conversation when it hits 70% of the context window (default i
131
131
 
132
132
  ### Watch all agents at once
133
133
 
134
- Use `shipwright status` (alias: `sw`, `cct`) to see a dashboard of running team sessions:
134
+ Use `shipwright status` (alias: `sw`) to see a dashboard of running team sessions:
135
135
 
136
136
  ```bash
137
137
  shipwright status
@@ -85,7 +85,7 @@ Each agent writes its results to a markdown file in this directory. The team lea
85
85
 
86
86
  ## Quick Start
87
87
 
88
- Pick a pattern, then use `shipwright` (alias: `sw`, `cct`) to set up the team:
88
+ Pick a pattern, then use `shipwright` (alias: `sw`) to set up the team:
89
89
 
90
90
  ```bash
91
91
  # Start a tmux session
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "shipwright-cli",
3
- "version": "2.3.1",
3
+ "version": "3.0.0",
4
4
  "description": "Orchestrate autonomous Claude Code agent teams in tmux",
5
5
  "bin": {
6
- "shipwright": "./scripts/sw",
7
- "sw": "./scripts/sw",
8
- "cct": "./scripts/sw"
6
+ "shipwright": "scripts/sw",
7
+ "sw": "scripts/sw",
8
+ "cct": "scripts/sw"
9
9
  },
10
10
  "files": [
11
11
  "config/",
@@ -38,7 +38,17 @@
38
38
  "dashboard:test:coverage": "vitest run --config dashboard/vitest.config.ts --coverage",
39
39
  "test": "bash scripts/sw-agi-roadmap-test.sh && bash scripts/sw-activity-test.sh && bash scripts/sw-adaptive-test.sh && bash scripts/sw-adversarial-test.sh && bash scripts/sw-architecture-enforcer-test.sh && bash scripts/sw-auth-test.sh && bash scripts/sw-autonomous-test.sh && bash scripts/sw-changelog-test.sh && bash scripts/sw-checkpoint-test.sh && bash scripts/sw-ci-test.sh && bash scripts/sw-cleanup-test.sh && bash scripts/sw-code-review-test.sh && bash scripts/sw-connect-test.sh && bash scripts/sw-context-test.sh && bash scripts/sw-cost-test.sh && bash scripts/sw-daemon-test.sh && bash scripts/sw-dashboard-test.sh && bash scripts/sw-db-test.sh && bash scripts/sw-decompose-test.sh && bash scripts/sw-deps-test.sh && bash scripts/sw-developer-simulation-test.sh && bash scripts/sw-discovery-test.sh && bash scripts/sw-doc-fleet-test.sh && bash scripts/sw-docs-agent-test.sh && bash scripts/sw-docs-test.sh && bash scripts/sw-doctor-test.sh && bash scripts/sw-dora-test.sh && bash scripts/sw-durable-test.sh && bash scripts/sw-e2e-orchestrator-test.sh && bash scripts/sw-eventbus-test.sh && bash scripts/sw-feedback-test.sh && bash scripts/sw-fix-test.sh && bash scripts/sw-fleet-discover-test.sh && bash scripts/sw-fleet-test.sh && bash scripts/sw-fleet-viz-test.sh && bash scripts/sw-frontier-test.sh && bash scripts/sw-github-app-test.sh && bash scripts/sw-github-checks-test.sh && bash scripts/sw-github-deploy-test.sh && bash scripts/sw-github-graphql-test.sh && bash scripts/sw-guild-test.sh && bash scripts/sw-heartbeat-test.sh && bash scripts/sw-hygiene-test.sh && bash scripts/sw-incident-test.sh && bash scripts/sw-init-test.sh && bash scripts/sw-instrument-test.sh && bash scripts/sw-intelligence-test.sh && bash scripts/sw-jira-test.sh && bash scripts/sw-launchd-test.sh && bash scripts/sw-linear-test.sh && bash scripts/sw-logs-test.sh && bash scripts/sw-loop-test.sh && bash scripts/sw-memory-test.sh && bash scripts/sw-mission-control-test.sh && bash scripts/sw-model-router-test.sh && bash scripts/sw-otel-test.sh && bash scripts/sw-oversight-test.sh && bash scripts/sw-patrol-meta-test.sh && bash scripts/sw-pipeline-composer-test.sh && bash scripts/sw-pipeline-test.sh && bash scripts/sw-pipeline-vitals-test.sh && bash scripts/sw-pm-test.sh && bash scripts/sw-pr-lifecycle-test.sh && bash scripts/sw-predictive-test.sh && bash scripts/sw-prep-test.sh && bash scripts/sw-ps-test.sh && bash scripts/sw-public-dashboard-test.sh && bash scripts/sw-quality-test.sh && bash scripts/sw-reaper-test.sh && bash scripts/sw-recruit-test.sh && bash scripts/sw-regression-test.sh && bash scripts/sw-release-manager-test.sh && bash scripts/sw-release-test.sh && bash scripts/sw-remote-test.sh && bash scripts/sw-replay-test.sh && bash scripts/sw-retro-test.sh && bash scripts/sw-scale-test.sh && bash scripts/sw-security-audit-test.sh && bash scripts/sw-self-optimize-test.sh && bash scripts/sw-session-test.sh && bash scripts/sw-setup-test.sh && bash scripts/sw-standup-test.sh && bash scripts/sw-status-test.sh && bash scripts/sw-strategic-test.sh && bash scripts/sw-stream-test.sh && bash scripts/sw-swarm-test.sh && bash scripts/sw-team-stages-test.sh && bash scripts/sw-templates-test.sh && bash scripts/sw-testgen-test.sh && bash scripts/sw-tmux-pipeline-test.sh && bash scripts/sw-tmux-test.sh && bash scripts/sw-trace-test.sh && bash scripts/sw-tracker-test.sh && bash scripts/sw-triage-test.sh && bash scripts/sw-upgrade-test.sh && bash scripts/sw-ux-test.sh && bash scripts/sw-webhook-test.sh && bash scripts/sw-widgets-test.sh && bash scripts/sw-worktree-test.sh && bash scripts/sw-policy-e2e-test.sh && bash scripts/sw-e2e-smoke-test.sh && bash scripts/sw-dashboard-e2e-test.sh",
40
40
  "test:smoke": "bash scripts/sw-e2e-smoke-test.sh",
41
- "test:integration": "bash scripts/sw-e2e-integration-test.sh"
41
+ "test:integration": "bash scripts/sw-e2e-integration-test.sh",
42
+ "harness:evidence:capture": "bash scripts/sw-evidence.sh capture",
43
+ "harness:evidence:verify": "bash scripts/sw-evidence.sh verify",
44
+ "harness:evidence:pre-pr": "bash scripts/sw-evidence.sh pre-pr",
45
+ "harness:evidence:capture:browser": "bash scripts/sw-evidence.sh capture browser",
46
+ "harness:evidence:capture:api": "bash scripts/sw-evidence.sh capture api",
47
+ "harness:evidence:capture:database": "bash scripts/sw-evidence.sh capture database",
48
+ "harness:evidence:capture:cli": "bash scripts/sw-evidence.sh capture cli",
49
+ "harness:ui:capture-browser-evidence": "bash scripts/sw-evidence.sh capture browser",
50
+ "harness:ui:verify-browser-evidence": "bash scripts/sw-evidence.sh verify",
51
+ "harness:ui:pre-pr": "bash scripts/sw-evidence.sh pre-pr browser"
42
52
  },
43
53
  "keywords": [
44
54
  "claude",