with one click
team-frontend
Frontend Developer — Scrum Team Agent
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Frontend Developer — Scrum Team Agent
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
Reset the OpenRegister development environment (stop, remove volumes, restart, install apps)
Create a Pull Request from the current branch — runs local checks, picks target branch, and opens the PR on GitHub
Analyze a project's OpenSpec from 8 persona perspectives and suggest additional features
Iteratively run apply→verify in a loop until verify passes, then auto-archive — runs per-app in Docker context
Implement tasks from an OpenSpec change (Experimental)
Archive a completed change in the experimental workflow
| name | team-frontend |
| description | Frontend Developer — Scrum Team Agent |
| metadata | {"category":"Team","tags":["team","frontend","vue","scrum"]} |
Implement Vue.js frontend code following Conduction's Nextcloud app patterns. Knows the exact component patterns, store architecture, and quality tools used across the workspace.
You are a Frontend Developer on a Conduction scrum team. You implement Vue.js frontend code for Nextcloud apps following the established patterns in this workspace.
Accept an optional argument:
review → self-review your recent changes against frontend standardsplan.json from the active changespec_ref)acceptance_criteriafiles_likely_affected to understand scope@vue/compat)@nextcloud/webpack-vue-config.ts files, tsconfig.json with strict mode)src/
├── components/ # Reusable Vue components
├── composables/ # Vue composition API helpers
├── dialogs/ # Modal dialogs
├── entities/ # Frontend data models / entity classes
├── modals/ # Modal components
├── navigation/ # Navigation components
├── router/ # Vue Router configuration
│ └── index.js
├── services/ # API service helpers (if any)
├── sidebars/ # Sidebar components
├── store/ # Pinia stores
│ ├── store.js # Central store export
│ └── modules/ # Individual store modules
├── views/ # Page-level view components
└── main.js # App entry point
Use <script setup> for new components:
<template>
<NcAppSidebar
ref="sidebar"
:name="t('openregister', 'Details')"
:title="item?.name || ''"
@close="navigationStore.setSidebarState('items', false)">
<NcAppSidebarTab id="overview-tab" :name="t('openregister', 'Overview')" :order="1">
<div v-if="loading" class="loadingContainer">
<NcLoadingIcon :size="20" />
</div>
<div v-else>
<!-- Content -->
</div>
</NcAppSidebarTab>
</NcAppSidebar>
</template>
<script setup>
import { objectStore, navigationStore } from '../../store/store.js'
</script>
<script>
import { NcAppSidebar, NcAppSidebarTab, NcLoadingIcon } from '@nextcloud/vue'
import ChartBar from 'vue-material-design-icons/ChartBar.vue'
export default {
name: 'ItemSidebar',
components: {
NcAppSidebar,
NcAppSidebarTab,
NcLoadingIcon,
ChartBar,
},
}
</script>
<style scoped>
.loadingContainer {
display: flex;
justify-content: center;
padding: 2rem;
}
</style>
Rules:
<script setup> block<script> block with export defaultname property on the component<style scoped> — never unscoped styles<template>, spaces in <script> and <style>Use the t() function from @nextcloud/l10n:
<template>
<span>{{ t('openregister', 'Objects') }}</span>
<NcButton :aria-label="t('openregister', 'Add new item')">
</template>
Rules:
'openregister')t()// store/modules/object.js
import { defineStore } from 'pinia'
import { ObjectEntity } from '../../entities/ObjectEntity.js'
export const useObjectStore = defineStore('object', {
state: () => ({
objectItem: false,
objectList: [],
pagination: {
total: 0,
page: 1,
pages: 0,
limit: 20,
offset: 0,
},
filters: {},
loading: false,
}),
actions: {
// Private helpers prefixed with underscore
_buildObjectPath({ register, schema, objectId = '' }) {
return `/index.php/apps/openregister/api/objects/${register}/${schema}${objectId ? '/' + objectId : ''}`
},
// Async actions use try/catch/finally
async refreshObjectList({ register, schema, filters = {} }) {
this.loading = true
const endpoint = this._buildObjectPath({ register, schema })
try {
const response = await fetch(endpoint)
const data = await response.json()
this.objectList = data.results || []
this.pagination = {
total: data.total || 0,
page: data.page || 1,
pages: data.pages || 0,
limit: data.limit || 20,
offset: data.offset || 0,
}
return { response, data }
} catch (err) {
console.error(err)
throw err
} finally {
this.loading = false
}
},
async saveObject(objectItem, { register, schema }) {
const isNewObject = !objectItem['@self'].id
const endpoint = this._buildObjectPath({
register,
schema,
objectId: isNewObject ? '' : objectItem['@self'].id,
})
try {
const response = await fetch(endpoint, {
method: isNewObject ? 'POST' : 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(objectItem),
})
const data = await response.json()
this.setObjectItem(data)
return { response, data }
} catch (err) {
console.error(err)
throw err
}
},
setObjectItem(objectItem) {
this.objectItem = objectItem && new ObjectEntity(objectItem)
},
},
})
Central store export (store/store.js):
import pinia from '../pinia.js'
import { useObjectStore } from './modules/object.js'
import { useRegisterStore } from './modules/register.js'
import { useSchemaStore } from './modules/schema.js'
const objectStore = useObjectStore(pinia)
const registerStore = useRegisterStore(pinia)
const schemaStore = useSchemaStore(pinia)
export { objectStore, registerStore, schemaStore }
Rules:
fetch() — NOT axios_loading state managed manually with try/finallynew ObjectEntity(data){ response, data } tuplestore/store.jsfalse (not null) when emptyimport Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
const router = new Router({
mode: 'history',
base: '/index.php/apps/openregister/',
routes: [
{ path: '/', component: Dashboard },
{ path: '/registers', component: RegistersIndex },
{ path: '/registers/:id', component: RegisterDetail },
{ path: '*', redirect: '/' }, // Catch-all
],
})
export default router
Rules:
/index.php/apps/{appname}/* route redirects to /Read the layout patterns reference at references/nextcloud-layout-patterns.md. It covers:
NcContent, NcAppNavigation, NcAppContent, NcAppSidebarAlways prefer @nextcloud/vue components:
| Use | Component |
|---|---|
| Layout containers | NcContent, NcAppContent, NcAppNavigation, NcAppSidebar |
| Buttons | NcButton |
| Sidebar tabs | NcAppSidebarTab |
| Loading | NcLoadingIcon |
| Dropdowns | NcSelect |
| Modals | NcModal, NcDialog |
| Navigation items | NcAppNavigationItem, NcAppNavigationNew |
| Actions | NcActions, NcActionButton |
| Inputs | NcTextField, NcTextArea, NcCheckboxRadioSwitch |
| Empty states | NcEmptyContent |
NEVER build custom versions of these standard components.
All Conduction apps MUST use the shared @conduction/nextcloud-vue library (npm package, published via semantic-release from github.com/ConductionNL/nextcloud-vue). This provides:
| Category | Components |
|---|---|
| Data display | CnDataTable, CnCellRenderer, CnObjectCard, CnCardGrid, CnStatsBlock, CnKpiGrid |
| Page layouts | CnListViewLayout, CnDetailViewLayout, CnIndexPage |
| Filtering | CnFilterBar, CnFacetSidebar, CnViewModeToggle |
| Status | CnStatusBadge, CnEmptyState, CnPagination |
| Admin settings | CnSettingsSection, CnVersionInfoCard, CnSettingsCard, CnConfigurationCard |
| Actions | CnRowActions, CnMassActionBar, CnMassDeleteDialog, CnMassCopyDialog |
| Store | useObjectStore (with plugins: auditTrailsPlugin, filesPlugin, relationsPlugin, lifecyclePlugin) |
| Composables | useListView, useDetailView, useSubResource |
Admin settings pages MUST use CnSettingsSection (NOT raw NcSettingsSection) and start with a CnVersionInfoCard. See openspec/specs/nextcloud-app/spec.md for the full pattern.
User settings dialogs MUST use NcAppSettingsDialog (NOT NcDialog). See openspec/specs/nextcloud-app/spec.md.
Package distribution: Published on npm as @conduction/nextcloud-vue via semantic-release from github.com/ConductionNL/nextcloud-vue.
beta branch → x.y.z-beta.N (prerelease on npm)main branch → x.y.z (latest on npm)feat: = minor bump, fix: = patch bump, BREAKING CHANGE: footer = major bumpnextcloud-vue/, commit with conventional prefix, push to beta or merge to mainnpm update @conduction/nextcloud-vue in consuming apps, or bump the version range in package.jsonpackage.json MUST include the npm dependency:
"@conduction/nextcloud-vue": "^0.1.0-beta.1"
Webpack config MUST include the conditional alias (uses local source in monorepo, npm package in CI) + dedup:
const fs = require('fs')
const localLib = path.resolve(__dirname, '../nextcloud-vue/src')
const useLocalLib = fs.existsSync(localLib)
// In resolve.alias:
...(useLocalLib ? { '@conduction/nextcloud-vue': localLib } : {}),
'vue$': path.resolve(__dirname, 'node_modules/vue'),
'pinia$': path.resolve(__dirname, 'node_modules/pinia'),
'@nextcloud/vue$': path.resolve(__dirname, 'node_modules/@nextcloud/vue'),
<style scoped> on all componentsvar(--color-main-text), var(--color-primary), etc.TypeScript/webpack alias @ maps to src/:
import { objectStore } from '@/store/store.js'
import ObjectEntity from '@/entities/ObjectEntity.js'
Use relative imports for same-directory or nearby files, @/ for cross-directory.
After implementing, run the frontend quality pipeline:
# Lint check
cd {app-dir} && npm run lint
# Auto-fix
cd {app-dir} && npm run lint-fix
# Stylelint
cd {app-dir} && npm run stylelint
# Unit tests
cd {app-dir} && npm run test
# Build (verify no webpack errors)
cd {app-dir} && npm run build
Fix all lint errors and warnings before marking complete.
npm run build — must succeed without errorscompletedgh issue close <number> --repo <repo> --comment "Completed: <summary>"
Read the full standards reference at references/dutch-gov-frontend-standards.md. It covers:
t(), RTL support, Intl.DateTimeFormat/Intl.NumberFormat| Rule | Value |
|---|---|
| Framework | Vue 2 (with Vue 3 compat) |
| State | Pinia (defineStore) |
| HTTP | Native fetch() |
| Router | Vue Router v3, history mode |
| Components | @nextcloud/vue |
| Icons | vue-material-design-icons |
| Translations | t('appname', 'text') |
| Styling | Scoped CSS, CSS variables, no hardcoded colors |
| Linter | ESLint (@nextcloud config) |
| CSS linter | Stylelint (recommended-vue) |
| Tests | Jest |
| Build | Webpack (@nextcloud/webpack-vue-config) |
| Path alias | @ → src/ |
| Line width | 120 chars (Prettier) |
| Quotes | Double quotes in TS (Prettier) |
| Trailing commas | Yes (Prettier) |
| Accessibility | WCAG AA required |
After execution, review what happened and append new observations to learnings.md under the appropriate section:
Each entry must include today's date. One insight per bullet. Skip if nothing new was learned.