aidevops 3.29.12 → 3.29.14

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.
package/README.md CHANGED
@@ -58,7 +58,7 @@ The result: an AI operations platform that manages projects across every busines
58
58
  [![Copyright](https://img.shields.io/badge/Copyright-Marcus%20Quinn%202025--2026-blue.svg)](https://github.com/marcusquinn)
59
59
 
60
60
  <!-- Release & Version Info -->
61
- [![Version](https://img.shields.io/badge/Version-3.29.12-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
61
+ [![Version](https://img.shields.io/badge/Version-3.29.14-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
62
62
  [![npm version](https://img.shields.io/npm/v/aidevops)](https://www.npmjs.com/package/aidevops)
63
63
  [![Homebrew](https://img.shields.io/badge/homebrew-marcusquinn%2Ftap-orange)](https://github.com/marcusquinn/homebrew-tap)
64
64
  [![GitHub repository](https://img.shields.io/badge/github-repository-181717.svg?logo=github)](https://github.com/marcusquinn/aidevops)
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.29.12
1
+ 3.29.14
package/aidevops.sh CHANGED
@@ -5,7 +5,7 @@
5
5
  # AI DevOps Framework CLI
6
6
  # Usage: aidevops <command> [options]
7
7
  #
8
- # Version: 3.29.12
8
+ # Version: 3.29.14
9
9
 
10
10
  set -euo pipefail
11
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aidevops",
3
- "version": "3.29.12",
3
+ "version": "3.29.14",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -3,12 +3,29 @@ import { sortConversationMessageParts, sortConversationMessages, type GuiConvers
3
3
  import { text } from "./app-model";
4
4
  import { conversationThreads } from "./conversation-fixtures";
5
5
 
6
- export function CommsConversationSurface({ mode }: { mode: "channels" | "directMessages" | "people" }) {
7
- const conversations = mode === "directMessages" ? conversationThreads.filter((thread) => thread.conversation.type === "dm" || thread.conversation.type === "group_dm") : conversationThreads.filter((thread) => thread.conversation.type === "channel");
8
- const selectedThread = conversations[0] ?? conversationThreads[0];
6
+ export function CommsConversationSurface({ mode, threads = conversationThreads }: { mode: "channels" | "directMessages" | "people"; threads?: GuiConversationThread[] }) {
7
+ const availableThreads = threads ?? [];
8
+ const conversations = mode === "directMessages" ? availableThreads.filter((thread) => thread.conversation.type === "dm" || thread.conversation.type === "group_dm") : availableThreads.filter((thread) => thread.conversation.type === "channel");
9
+ const selectedThread = conversations[0] ?? availableThreads[0];
9
10
  const partsByMessage = new Map<string, GuiConversationMessagePart[]>();
10
11
 
11
- for (const part of sortConversationMessageParts(selectedThread.parts)) {
12
+ if (!selectedThread) {
13
+ return (
14
+ <section className="chat-surface comms-surface" aria-label={mode === "directMessages" ? text.directMessages : text.channels} data-tour="comms-conversations">
15
+ <aside className="chat-thread-panel conversation-list-panel" aria-label="Conversation list">
16
+ <header className="chat-thread-header">
17
+ <div>
18
+ <p className="eyebrow">Unified conversation model</p>
19
+ <h2>{mode === "directMessages" ? <FiAtSign aria-hidden="true" /> : <FiHash aria-hidden="true" />} {mode === "directMessages" ? text.directMessages : text.channels}</h2>
20
+ </div>
21
+ </header>
22
+ <div className="notice compact-notice">No conversations yet</div>
23
+ </aside>
24
+ </section>
25
+ );
26
+ }
27
+
28
+ for (const part of sortConversationMessageParts(conversationParts(selectedThread))) {
12
29
  partsByMessage.set(part.message_id, [...(partsByMessage.get(part.message_id) ?? []), part]);
13
30
  }
14
31
 
@@ -37,10 +54,10 @@ export function CommsConversationSurface({ mode }: { mode: "channels" | "directM
37
54
  <span className="count-pill">{participantSummary(selectedThread)}</span>
38
55
  </header>
39
56
  <div className="participant-strip">
40
- {selectedThread.participants.map((participant) => <span key={participant.id}>{participant.display_name}<small>{participant.kind}</small></span>)}
57
+ {conversationParticipants(selectedThread).map((participant) => <span key={participant.id}>{participant.display_name}<small>{participant.kind}</small></span>)}
41
58
  </div>
42
59
  <div className="chat-message-list" data-tour="comms-message-timeline">
43
- {sortConversationMessages(selectedThread.messages).map((message) => <ConversationMessageRow key={message.id} message={message} parts={partsByMessage.get(message.id) ?? []} thread={selectedThread} />)}
60
+ {sortConversationMessages(conversationMessages(selectedThread)).map((message) => <ConversationMessageRow key={message.id} message={message} parts={partsByMessage.get(message.id) ?? []} thread={selectedThread} />)}
44
61
  </div>
45
62
  <form className="chat-composer" aria-label="Conversation composer">
46
63
  <textarea disabled placeholder={text.chatInputPlaceholder} />
@@ -52,7 +69,7 @@ export function CommsConversationSurface({ mode }: { mode: "channels" | "directM
52
69
  }
53
70
 
54
71
  function ConversationListItem({ thread }: { thread: GuiConversationThread }) {
55
- const latestMessage = sortConversationMessages(thread.messages).at(-1);
72
+ const latestMessage = sortConversationMessages(conversationMessages(thread)).at(-1);
56
73
  const unread = unreadCount(thread);
57
74
 
58
75
  return (
@@ -67,8 +84,8 @@ function ConversationListItem({ thread }: { thread: GuiConversationThread }) {
67
84
  }
68
85
 
69
86
  function ConversationMessageRow({ message, parts, thread }: { message: GuiConversationMessage; parts: GuiConversationMessagePart[]; thread: GuiConversationThread }) {
70
- const sender = thread.participants.find((participant) => participant.id === message.sender_participant_id);
71
- const reactions = thread.reactions.filter((reaction) => reaction.message_id === message.id);
87
+ const sender = conversationParticipants(thread).find((participant) => participant.id === message.sender_participant_id);
88
+ const reactions = conversationReactions(thread).filter((reaction) => reaction.message_id === message.id);
72
89
  const speaker = message.sender_kind === "human" ? "user" : "assistant";
73
90
 
74
91
  return (
@@ -82,12 +99,32 @@ function ConversationMessageRow({ message, parts, thread }: { message: GuiConver
82
99
  }
83
100
 
84
101
  function participantSummary(thread: GuiConversationThread): string {
85
- const active = thread.participants.filter((participant) => participant.membership_state === "active").length;
102
+ const active = conversationParticipants(thread).filter((participant) => participant.membership_state === "active").length;
86
103
  return `${active} members`;
87
104
  }
88
105
 
89
106
  function unreadCount(thread: GuiConversationThread): number {
90
- const lastSequence = Math.max(0, ...thread.messages.map((message) => message.sequence));
91
- const localRead = thread.read_states[0]?.last_read_sequence ?? 0;
107
+ const lastSequence = Math.max(0, ...conversationMessages(thread).map((message) => message.sequence));
108
+ const localRead = conversationReadStates(thread)[0]?.last_read_sequence ?? 0;
92
109
  return Math.max(0, lastSequence - localRead);
93
110
  }
111
+
112
+ function conversationMessages(thread: GuiConversationThread): GuiConversationMessage[] {
113
+ return thread.messages ?? [];
114
+ }
115
+
116
+ function conversationParts(thread: GuiConversationThread): GuiConversationMessagePart[] {
117
+ return thread.parts ?? [];
118
+ }
119
+
120
+ function conversationParticipants(thread: GuiConversationThread): GuiConversationThread["participants"] {
121
+ return thread.participants ?? [];
122
+ }
123
+
124
+ function conversationReactions(thread: GuiConversationThread): GuiConversationThread["reactions"] {
125
+ return thread.reactions ?? [];
126
+ }
127
+
128
+ function conversationReadStates(thread: GuiConversationThread): GuiConversationThread["read_states"] {
129
+ return thread.read_states ?? [];
130
+ }
@@ -29,6 +29,8 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
29
29
  const managedApps = sortedManagedApps(status.managed_apps).map((app) => applyManagedPolicyToggles(app, policyToggles[app.id]));
30
30
  const visibleManagedApps = managedApps.filter((app) => managedCategoryForApp(app) === managedCategory);
31
31
  const visibleRecommendedApps = recommendedApps.filter((app) => recommendedAppMatchesFilters(app, recommendedPlatform, recommendedOs));
32
+ const selectRecommendedPlatform = (value: RecommendedPlatformFilterId) => setRecommendedPlatform((current) => nextRecommendedFilterValue(current, value, "all"));
33
+ const selectRecommendedOs = (value: RecommendedOsId) => setRecommendedOs((current) => nextRecommendedFilterValue(current, value, "all"));
32
34
 
33
35
  useEffect(() => {
34
36
  setExpandedAppId(visibleManagedApps[0]?.id ?? null);
@@ -85,7 +87,7 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
85
87
  ))}
86
88
  </div>
87
89
  <p className="empty-state compact-notice">Install/update actions run as allowlisted background jobs and stream inside each app panel. xterm.js with a node-pty bridge is the right next step for full TUI apps such as OpenCode; this view starts with non-interactive command logs.</p>
88
- </> : <RecommendedAppsSurface apps={visibleRecommendedApps} os={recommendedOs} platform={recommendedPlatform} setOs={setRecommendedOs} setPlatform={setRecommendedPlatform} />}
90
+ </> : <RecommendedAppsSurface apps={visibleRecommendedApps} os={recommendedOs} platform={recommendedPlatform} setOs={selectRecommendedOs} setPlatform={selectRecommendedPlatform} />}
89
91
  </section>
90
92
  );
91
93
  }
@@ -120,11 +122,9 @@ function ManagedAppPanel({ app, expanded, job, onJob, onPolicyToggle, onToggle,
120
122
  <div className="managed-app-details">
121
123
  <ToggleSwitch checked={app.aidevops_install} disabled={lockedPolicy} label="setup installs" onChange={(value) => onPolicyToggle("setup", value)} />
122
124
  <ToggleSwitch checked={app.aidevops_update} disabled={lockedPolicy} label="update maintains" onChange={(value) => onPolicyToggle("update", value)} />
123
- <AppMeta label="Installed" value={app.installed_version} />
124
- <AppMeta label="Latest" value={app.latest_version} />
125
- <AppMeta label={text.path} value={app.install_path_ref} />
126
125
  {job ? <AppLogLink job={job} /> : null}
127
126
  </div>
127
+ <AppMeta className="managed-app-path" label={text.path} value={app.install_path_ref} />
128
128
  {job ? <AppActionTerminal job={job} /> : null}
129
129
  {policyJobs.map((policyJob) => <AppActionTerminal job={policyJob} key={policyJob.id} />)}
130
130
  {job === null && policyJobs.length === 0 ? <p className="empty-state compact-notice">No recent command output for this app. Run an action or change a policy toggle to open this app's terminal log.</p> : null}
@@ -146,11 +146,11 @@ function OriginLink({ href, label }: { href: string; label: string }): ReactElem
146
146
  }
147
147
 
148
148
  function ToggleSwitch({ checked, disabled = false, label, onChange }: { checked: boolean; disabled?: boolean; label: string; onChange: (checked: boolean) => void }): ReactElement {
149
- return <button aria-pressed={checked} className={checked ? "managed-toggle checked" : "managed-toggle"} data-tooltip={disabled ? "Essential aidevops component; policy is locked on" : undefined} disabled={disabled} onClick={() => onChange(!checked)} title={disabled ? "Essential aidevops component; policy is locked on" : undefined} type="button"><span aria-hidden="true" className={checked ? "switch-track checked" : "switch-track"}><span /></span>{label}</button>;
149
+ return <button aria-pressed={checked} className={checked ? "managed-toggle checked" : "managed-toggle"} data-tooltip={disabled ? "Essential aidevops component; policy is locked on" : undefined} disabled={disabled} onClick={() => onChange(!checked)} title={disabled ? "Essential aidevops component; policy is locked on" : undefined} type="button"><span className="managed-toggle-label">{label}</span><span aria-hidden="true" className={checked ? "switch-track checked" : "switch-track"}><span /></span></button>;
150
150
  }
151
151
 
152
- function AppMeta({ label, value }: { label: string; value: string }): ReactElement {
153
- return <span className="app-meta"><small>{label}</small><strong>{value}</strong></span>;
152
+ function AppMeta({ className = "", label, value }: { className?: string; label: string; value: string }): ReactElement {
153
+ return <span className={className.length > 0 ? `app-meta ${className}` : "app-meta"}><small>{label}</small><strong>{value}</strong></span>;
154
154
  }
155
155
 
156
156
  function AppLogLink({ job }: { job: GuiAppActionJobSummary }): ReactElement {
@@ -291,7 +291,7 @@ interface RecommendedApp {
291
291
  }
292
292
 
293
293
  const appCollectionTabs: TabOption<AppCollectionId>[] = [
294
- { id: "aidevops", label: "aidevops" },
294
+ { id: "aidevops", label: "AIDevOps" },
295
295
  { id: "recommended", label: "Recommended" },
296
296
  ];
297
297
 
@@ -379,8 +379,8 @@ function TabNav<T extends string>({ label, onChange, tabs, value }: { label: str
379
379
  function RecommendedAppsSurface({ apps, os, platform, setOs, setPlatform }: { apps: RecommendedApp[]; os: RecommendedOsId; platform: RecommendedPlatformFilterId; setOs: (os: RecommendedOsId) => void; setPlatform: (platform: RecommendedPlatformFilterId) => void }): ReactElement {
380
380
  return <>
381
381
  <div className="recommended-filter-tabs">
382
- <TabNav label="Recommended app platform filters" tabs={recommendedPlatformTabs} value={platform} onChange={setPlatform} />
383
382
  <TabNav label="Recommended app operating system filters" tabs={recommendedOsTabs} value={os} onChange={setOs} />
383
+ <TabNav label="Recommended app platform filters" tabs={recommendedPlatformTabs} value={platform} onChange={setPlatform} />
384
384
  </div>
385
385
  <div className="recommended-app-grid">
386
386
  {apps.map((app) => <RecommendedAppCard app={app} key={app.name} setOs={setOs} setPlatform={setPlatform} />)}
@@ -459,6 +459,10 @@ function recommendedAppMatchesFilters(app: RecommendedApp, platform: Recommended
459
459
  return platformMatches && osMatches;
460
460
  }
461
461
 
462
+ export function nextRecommendedFilterValue<T extends string>(current: T, next: T, defaultValue: T): T {
463
+ return current === next && next !== defaultValue ? defaultValue : next;
464
+ }
465
+
462
466
  function applyManagedPolicyToggles(app: GuiManagedAppSummary, policy: Partial<Record<ManagedPolicyId, boolean>> | undefined): GuiManagedAppSummary {
463
467
  if (policy === undefined) {
464
468
  return app;
@@ -147,7 +147,7 @@ export const text = {
147
147
  aidevops: "aidevops",
148
148
  appShell: "App shell",
149
149
  apps: "Apps",
150
- appsIntro: "App and CLI inventory for tools installed or updated by aidevops, with origin links, install/update policy, versions, paths, and allowlisted background actions.",
150
+ appsIntro: "The app toolkit we use and recommend to enable all the things we can do with AI, development, and operations to create and share our work. Opinionated preferences based on open-source-first, lowest-costs, highest-quality UI & UX, teamwork, data security, privacy, and AI-usage capabilities.",
151
151
  appStatusPending: "adapter pending",
152
152
  brands: "Brands",
153
153
  brandsIntro: "Draft brand inventory for names and website references.",
@@ -943,9 +943,8 @@ code {
943
943
  .header-actions > .header-action-menu {
944
944
  align-items: center;
945
945
  display: inline-flex;
946
- flex: 0 0 38px;
946
+ flex: 0 0 auto;
947
947
  justify-content: center;
948
- width: 38px;
949
948
  }
950
949
 
951
950
  .workspace-search {
@@ -1001,6 +1000,7 @@ code {
1001
1000
  }
1002
1001
 
1003
1002
  .header-action-menu {
1003
+ gap: 10px;
1004
1004
  position: relative;
1005
1005
  }
1006
1006
 
@@ -1784,15 +1784,23 @@ code {
1784
1784
  }
1785
1785
 
1786
1786
  .section-heading {
1787
- margin-bottom: 18px;
1787
+ margin-bottom: 16px;
1788
1788
  max-width: 860px;
1789
1789
  }
1790
1790
 
1791
+ .section-heading .eyebrow {
1792
+ margin: 0 0 9px;
1793
+ }
1794
+
1791
1795
  .section-heading h2 {
1792
1796
  font-size: 1.7rem;
1793
1797
  letter-spacing: -0.05em;
1794
1798
  line-height: 1.04;
1795
- margin: 6px 0 8px;
1799
+ margin: 0 0 16px;
1800
+ }
1801
+
1802
+ .section-heading p {
1803
+ margin: 0;
1796
1804
  }
1797
1805
 
1798
1806
  .section-heading p,
@@ -2003,13 +2011,25 @@ code {
2003
2011
  }
2004
2012
 
2005
2013
  .apps-surface {
2006
- display: grid;
2007
2014
  align-content: start;
2008
- gap: 10px;
2015
+ display: grid;
2016
+ gap: 0;
2009
2017
  }
2010
2018
 
2011
2019
  .apps-surface .section-heading {
2012
- margin-bottom: 8px;
2020
+ margin-bottom: 18px;
2021
+ }
2022
+
2023
+ .apps-surface > .app-subnav {
2024
+ margin-bottom: 18px;
2025
+ }
2026
+
2027
+ .apps-surface > .app-subnav + .app-subnav {
2028
+ margin-bottom: 16px;
2029
+ }
2030
+
2031
+ .apps-surface .compact-notice {
2032
+ margin-bottom: 0;
2013
2033
  }
2014
2034
 
2015
2035
  .data-table,
@@ -2021,8 +2041,8 @@ code {
2021
2041
  }
2022
2042
 
2023
2043
  .managed-app-list {
2024
- gap: 12px;
2025
- margin-bottom: 12px;
2044
+ gap: 16px;
2045
+ margin-bottom: 16px;
2026
2046
  overflow: visible;
2027
2047
  }
2028
2048
 
@@ -2033,7 +2053,7 @@ code {
2033
2053
  border: 1px solid var(--border);
2034
2054
  border-radius: 18px;
2035
2055
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 7%);
2036
- overflow: hidden;
2056
+ overflow: visible;
2037
2057
  transition: background 140ms ease, border-color 140ms ease;
2038
2058
  }
2039
2059
 
@@ -2071,14 +2091,23 @@ code {
2071
2091
  .managed-app-title-block,
2072
2092
  .app-meta {
2073
2093
  display: grid;
2074
- gap: 7px;
2075
2094
  min-width: 0;
2076
2095
  }
2077
2096
 
2097
+ .managed-app-title-block {
2098
+ gap: 0;
2099
+ }
2100
+
2101
+ .managed-app-title-block .eyebrow {
2102
+ line-height: 1;
2103
+ margin: 0 0 8px;
2104
+ }
2105
+
2078
2106
  .managed-app-title-block strong {
2079
2107
  font-size: clamp(1.12rem, 2vw, 1.4rem);
2080
2108
  letter-spacing: -0.04em;
2081
2109
  line-height: 1.15;
2110
+ margin-bottom: 16px;
2082
2111
  }
2083
2112
 
2084
2113
  .managed-app-title-block > span:last-child {
@@ -2097,14 +2126,16 @@ code {
2097
2126
 
2098
2127
  .managed-summary-chip {
2099
2128
  align-items: center;
2129
+ align-content: center;
2100
2130
  background: color-mix(in srgb, var(--surface-elevated) 92%, transparent);
2101
2131
  border: 1px solid var(--border);
2102
2132
  border-radius: 999px;
2103
2133
  display: inline-grid;
2104
2134
  gap: 1px;
2105
2135
  justify-items: center;
2136
+ min-height: 58px;
2106
2137
  min-width: 118px;
2107
- padding: 7px 11px;
2138
+ padding: 7px 12px;
2108
2139
  text-align: center;
2109
2140
  }
2110
2141
 
@@ -2120,6 +2151,7 @@ code {
2120
2151
  color: var(--text-primary);
2121
2152
  font-size: 0.84rem;
2122
2153
  font-weight: 800;
2154
+ line-height: 1.15;
2123
2155
  max-width: 18ch;
2124
2156
  overflow: hidden;
2125
2157
  text-overflow: ellipsis;
@@ -2139,7 +2171,7 @@ code {
2139
2171
  .managed-app-body {
2140
2172
  border-top: 1px solid var(--border);
2141
2173
  display: grid;
2142
- gap: 14px;
2174
+ gap: 16px;
2143
2175
  padding: 0 16px 16px;
2144
2176
  position: relative;
2145
2177
  }
@@ -2147,9 +2179,9 @@ code {
2147
2179
  .managed-app-toolbar {
2148
2180
  align-items: start;
2149
2181
  display: grid;
2150
- gap: 12px;
2182
+ gap: 16px;
2151
2183
  grid-template-columns: minmax(0, 1fr) auto;
2152
- padding-top: 14px;
2184
+ padding-top: 16px;
2153
2185
  }
2154
2186
 
2155
2187
  .managed-app-links,
@@ -2195,11 +2227,16 @@ code {
2195
2227
  grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
2196
2228
  }
2197
2229
 
2230
+ .managed-app-path {
2231
+ min-height: auto;
2232
+ }
2233
+
2198
2234
  .managed-toggle,
2199
2235
  .app-meta {
2200
2236
  background: var(--surface-elevated);
2201
2237
  border: 1px solid var(--border);
2202
2238
  border-radius: 12px;
2239
+ gap: 7px;
2203
2240
  min-width: 0;
2204
2241
  min-height: 64px;
2205
2242
  padding: 10px 12px;
@@ -2211,10 +2248,15 @@ code {
2211
2248
  display: flex;
2212
2249
  font-size: 0.78rem;
2213
2250
  font-weight: 800;
2214
- gap: 8px;
2251
+ gap: 12px;
2252
+ justify-content: space-between;
2215
2253
  text-transform: uppercase;
2216
2254
  }
2217
2255
 
2256
+ .managed-toggle-label {
2257
+ min-width: 0;
2258
+ }
2259
+
2218
2260
  button.managed-toggle {
2219
2261
  cursor: pointer;
2220
2262
  text-align: left;
@@ -2236,6 +2278,7 @@ button.managed-toggle:focus-visible {
2236
2278
  border: 1px solid color-mix(in srgb, #ef4444 44%, var(--border));
2237
2279
  border-radius: 999px;
2238
2280
  display: inline-flex;
2281
+ flex: 0 0 auto;
2239
2282
  padding: 2px;
2240
2283
  width: 34px;
2241
2284
  }
@@ -2293,19 +2336,27 @@ button.managed-toggle:focus-visible {
2293
2336
  color: var(--accent);
2294
2337
  display: inline-flex;
2295
2338
  gap: 7px;
2296
- min-height: 42px;
2297
2339
  justify-content: center;
2340
+ line-height: 1;
2341
+ min-height: 42px;
2298
2342
  padding: 0 12px;
2299
2343
  position: relative;
2300
2344
  transition: background 140ms ease, border-color 140ms ease, color 140ms ease, transform 140ms ease;
2301
2345
  }
2302
2346
 
2303
2347
  .app-action-button span {
2348
+ align-items: center;
2349
+ display: inline-flex;
2304
2350
  font-size: 0.7rem;
2305
2351
  font-weight: 900;
2352
+ line-height: 1;
2306
2353
  text-transform: uppercase;
2307
2354
  }
2308
2355
 
2356
+ .app-action-button svg {
2357
+ flex: 0 0 auto;
2358
+ }
2359
+
2309
2360
  .app-action-button:hover,
2310
2361
  .app-action-button:focus-visible {
2311
2362
  background: var(--accent);
@@ -2421,7 +2472,7 @@ button.managed-toggle:focus-visible {
2421
2472
  content: attr(data-tooltip);
2422
2473
  font-size: 0.72rem;
2423
2474
  font-weight: 700;
2424
- left: clamp(10px, 50%, calc(100vw - 330px));
2475
+ left: 50%;
2425
2476
  line-height: 1.35;
2426
2477
  max-width: min(320px, 72vw);
2427
2478
  opacity: 0;
@@ -2432,7 +2483,7 @@ button.managed-toggle:focus-visible {
2432
2483
  transform: translate(-50%, 4px);
2433
2484
  transition: opacity 140ms ease, transform 140ms ease;
2434
2485
  width: max-content;
2435
- z-index: 400;
2486
+ z-index: 1000;
2436
2487
  }
2437
2488
 
2438
2489
  [data-tooltip]:hover::after,
@@ -2526,7 +2577,8 @@ button.managed-toggle:focus-visible {
2526
2577
  .recommended-filter-tabs {
2527
2578
  display: flex;
2528
2579
  flex-wrap: wrap;
2529
- gap: 10px;
2580
+ gap: 16px;
2581
+ margin-bottom: 16px;
2530
2582
  }
2531
2583
 
2532
2584
  .recommended-app-grid {
package/setup.sh CHANGED
@@ -12,7 +12,7 @@ shopt -s inherit_errexit 2>/dev/null || true
12
12
  # AI Assistant Server Access Framework Setup Script
13
13
  # Helps developers set up the framework for their infrastructure
14
14
  #
15
- # Version: 3.29.12
15
+ # Version: 3.29.14
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)
@@ -1330,6 +1330,8 @@ _setup_run_non_interactive() {
1330
1330
  # at commit time (t2047). Belt-and-braces with the CI check in
1331
1331
  # .github/workflows/task-id-collision-check.yml.
1332
1332
  _time_step "setup_task_id_guard" setup_task_id_guard
1333
+ # Configure the centralized flat linked-worktree directory used by helpers.
1334
+ _time_step "setup_worktree_location" setup_worktree_location
1333
1335
  # Apply Spotlight + Time Machine exclusions to every worktree across
1334
1336
  # registered repos so the backup/index cascade triggered by node_modules
1335
1337
  # copies doesn't burn CPU (t2885). Idempotent. macOS only — Linux