diff --git a/.project-docs/30-worklog/tasks/20260905-official-plugin-project-scope-6e4a9c21.md b/.project-docs/30-worklog/tasks/20260905-official-plugin-project-scope-6e4a9c21.md new file mode 100644 index 0000000..42e3aad --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260905-official-plugin-project-scope-6e4a9c21.md @@ -0,0 +1,67 @@ +# Task: Make official project Plugins project-wide + +## Identity + +- Task ID: 20260905-official-plugin-project-scope-6e4a9c21 +- Mode: Feature +- Branch: codex/20260905-official-plugin-project-scope-6e4a9c21-official-plugin-project-scope +- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260905-official-plugin-project-scope-6e4a9c21 +- Base commit: 5e0a091955da7aadcf0c70a1a266fc3aa077359a +- Owner: codex-root +- Status: Ready for Integration + +## Scope + +- Reproduce the official Game Resource Plugin still exposing Agent-assignment UI and runtime `skill_unassigned` gating after project enablement. +- Make MakeLore code-owned official project Plugins project-wide: Data Service, Game Resource, and Project Scaffold materialize their Skills/tools for the parent Agent when enabled for a project, without a separate partner assignment. +- Keep third-party Marketplace and locally installed packages on their existing assignment/lifecycle rules, and keep child Agents from inheriting parent-only Plugin resources. +- Update the shared project-wide predicate, Renderer projection/action tests, effective resolver tests, and directly affected end-to-end expectations only. + +## Intent And Constraints + +- User-confirmed product rule: project enablement is the only project-level activation step for official Plugins; the UI must not offer a redundant “assign to partner” action. +- Main remains the authority for effective worker resources; Renderer only projects the same shared predicate and dispatches existing commands. +- Account acquisition, device/package availability, project enablement, provider admission, billing, and parent/child lifecycle semantics remain unchanged. +- No compatibility layer, feature flag, new store, server change, Marketplace policy change, or local-package behavior change. +- Current accepted ADR/project memory says Project Scaffold is the sole exception and Game Resource retains assignment. This task treats the user's newer instruction as the superseding product decision and records the required canonical-doc update as an Integration promotion candidate rather than editing canonical memory in Feature mode. +- Concurrent Task Gate: Passed. `task_context.py start` created this managed worktree from exact base `5e0a091955da7aadcf0c70a1a266fc3aa077359a`; status identity matched and no active peer owned overlapping files or semantics. +- Planning Gate: Passed after loading the active task record, project entry documents, current state, ADR-008/decision index, architecture/data flow/module map, business rules, success criteria, glossary, evidence, reflection, commitments, and stale items. +- Project Context Loaded: + - MakeLore Electron Main owns effective Plugin resources; the Renderer must project the same shared activation rule rather than inventing a second lifecycle. + - The integrated state made only Project Scaffold project-wide, while Game Resource and Data Service still emitted assignment UI and `skill_unassigned` runtime outcomes. + - Account acquisition, code-owned bundled delivery, project enablement, policy/admission, billing, and frozen worker generations remain separate authorities. + - The user explicitly superseded the earlier narrow exception by requiring official project Plugins to work from project enablement alone. + - The supported boundary is the three code-owned official identities; downloaded Marketplace and local Device Packages retain their existing behavior. +- TDD boundary: reproduce the public resolver and workspace command failures first, then change the single shared predicate used by both Main and Renderer. + +## Outcome + +- Confirmed the screenshot was not a stale rendering issue: the shared project-wide predicate contained only Project Scaffold. Game Resource and Data Service therefore both produced a Renderer `open_agent_assignment` command and a Main `skill_unassigned` outcome when enabled without an Agent assignment. +- Extended that single code-owned predicate to Data Service, Game Resource, and Project Scaffold. Once their existing acquisition/delivery and project-enable requirements are met, every parent Agent receives their Skills/tools without partner assignment; child Agent behavior remains unchanged. +- Updated the unified Plugin workspace fixtures and Electron journey so code-owned official cards/details show `随项目启用` / `生效范围` and never expose a partner-assignment button. Third-party official Plugin tests continue to retain `open_agent_assignment`. +- No server, Marketplace contract, project persistence, Provider, billing, Package Store, local Plugin, or worker-lifecycle authority changed. + +## Verification + +- TDD RED: two focused files produced 4 expected failures and 26 passes. Game Resource and Data Service returned `skill_unassigned`; both system-included and bundled Renderer projections still emitted `open_agent_assignment`. +- Focused GREEN: resolver and workspace model passed 30/30. +- Adjacent Plugin/runtime/page slice: 6 files, 70/70 passed; the effective-resolver/composition/capability/product-host subset passed 37/37. +- `corepack pnpm run typecheck`: passed. +- Scoped ESLint and `git diff --check`: passed. Full `corepack pnpm run lint:check` passed with 0 errors and 5 unchanged warnings in untouched Home/Makelore files. +- Full unit command completed with 1,889 passes and 2 skips; one unrelated two-second real-process timing assertion measured 2,773 ms. The exact failed file then passed 6/6 in isolation. +- `corepack pnpm run build:vite`: Renderer, Main, Preload, and utility builds passed with existing Browserslist/import/chunk warnings. +- Targeted production-build Electron journey `tests/e2e/project-plugins.spec.ts`: 1/1 passed, proving project enable/disable while the detail reports project-wide scope and contains no Data Service assignment button. + +## Follow-ups + +- Integration must promote the superseding official project-wide rule into canonical memory, merge the source commit to local `main`, and preserve the root worktree's three pre-existing untracked task records. +- A rebuilt/installed client smoke is still required before claiming the user's currently installed binary contains the change. + +## Promotion Candidates + +- Targets: ADR-008, decision index, current state, system overview, module map, data flow, business rules, success criteria, README, evidence, and the installed-client release commitment. +- Proposal: all MakeLore code-owned official project Plugins (`makelore.data-service`, `makelore.game-resource`, and `makelore.project-scaffold`) are project-wide. Existing Account acquisition/delivery and project enablement remain required where applicable, but Agent assignment is neither required nor presented. Downloaded Marketplace and local packages retain current assignment/lifecycle behavior. +- Evidence: the focused RED reproduced both Main `skill_unassigned` and Renderer assignment commands; the shared-predicate fix passed focused, adjacent, full-unit diagnostics, production builds, and the Electron project journey. +- Future impact: new code-owned official project Plugin identities must be added to the shared project-wide predicate and tested at both Main effective resolution and Renderer command projection. Existing stored assignments for these three identities may remain inert and preserved. +- Semantic conflict: this supersedes ADR-008 and current architecture/domain text that name Project Scaffold as the sole exception and explicitly require Game Resource/Data Service assignment. +- Human confirmation required: No. The user explicitly corrected the behavior after observing the Game Resource assignment button on 2026-09-05. diff --git a/shared/coding-plugins.ts b/shared/coding-plugins.ts index 7b0b7e4..d7c89ea 100644 --- a/shared/coding-plugins.ts +++ b/shared/coding-plugins.ts @@ -112,7 +112,9 @@ export const PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID = '00000000-0000-4000-8000-0000 * Agent Skill assignment. */ export function isProjectWideCodingPluginId(pluginId: string): boolean { - return pluginId === PROJECT_SCAFFOLD_PLUGIN_ID; + return pluginId === DATA_SERVICE_PLUGIN_ID + || pluginId === GAME_RESOURCE_PLUGIN_ID + || pluginId === PROJECT_SCAFFOLD_PLUGIN_ID; } export const CODE_OWNED_OPTIONAL_BUNDLED_RELEASES = Object.freeze({ diff --git a/tests/e2e/project-plugins.spec.ts b/tests/e2e/project-plugins.spec.ts index f9c9765..a5b6085 100644 --- a/tests/e2e/project-plugins.spec.ts +++ b/tests/e2e/project-plugins.spec.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron'; test.describe('Unified project plugin workspace', () => { - test('enables and disables a project plugin without configuring it, then opens Agent assignment', async ({ launchElectronApp }) => { + test('enables and disables a project-wide official plugin without configuring or assigning it', async ({ launchElectronApp }) => { const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-project-plugins-e2e-')); const app = await launchElectronApp({ skipSetup: true }); try { @@ -94,6 +94,9 @@ test.describe('Unified project plugin workspace', () => { await expect(page).toHaveURL(/\/plugins\?scope=project/); await page.getByRole('button', { name: '查看开发数据服务详情' }).click(); await expect(page.getByText('按平台包含,不按单次插件调用扣点。')).toBeVisible(); + await expect(page.getByRole('heading', { name: '生效范围' })).toBeVisible(); + await expect(page.getByText(/启用当前项目后.*无需单独分配/)).toBeVisible(); + await expect(page.getByRole('button', { name: '分配开发数据服务给伙伴' })).toHaveCount(0); const localProjectId = await app.evaluate(() => ( (globalThis as typeof globalThis & { __projectPluginE2E?: { localProjectId: string } }).__projectPluginE2E?.localProjectId )); @@ -111,8 +114,6 @@ test.describe('Unified project plugin workspace', () => { await page.getByRole('button', { name: '确认从当前项目禁用' }).click(); await expect(page.getByRole('button', { name: '启用开发数据服务到当前项目' })).toBeVisible(); - await page.getByRole('button', { name: '分配开发数据服务给伙伴' }).click(); - await expect(page.getByTestId('project-configuration-page')).toBeVisible(); const state = await app.evaluate(() => ( (globalThis as typeof globalThis & { __projectPluginE2E?: { dataServiceCalls: number; mutations: Array<{ enabled: boolean; projectId: string }> }; diff --git a/tests/unit/coding-plugin-effective-resolver.test.ts b/tests/unit/coding-plugin-effective-resolver.test.ts index 6c189b3..1c7c773 100644 --- a/tests/unit/coding-plugin-effective-resolver.test.ts +++ b/tests/unit/coding-plugin-effective-resolver.test.ts @@ -7,6 +7,8 @@ import { } from '../../electron/coding-plugins/effective-resolver'; import { DATA_SERVICE_PLUGIN_DEFINITION, + GAME_RESOURCE_BUNDLED_RELEASE_ID, + GAME_RESOURCE_PLUGIN_ID, PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID, PROJECT_SCAFFOLD_PLUGIN_ID, type CodingPluginDefinition, @@ -450,11 +452,11 @@ describe('effective plugin resolver', () => { expect(refresh).not.toHaveBeenCalled(); }); - it('materializes an acquired code-owned bundled hosted Plugin without Package Store bytes', async () => { + it('materializes an acquired code-owned bundled hosted Plugin without Package Store bytes or Agent assignment', async () => { const bundled: CodingPluginDefinition = { ...serverDefinition, - id: 'makelore.game-resource', - releaseId: '00000000-0000-4000-8000-000000000105', + id: GAME_RESOURCE_PLUGIN_ID, + releaseId: GAME_RESOURCE_BUNDLED_RELEASE_ID, provenance: { source: 'bundled', packageRoot: 'game-resource' }, skills: [{ id: 'game-resource', @@ -485,7 +487,7 @@ describe('effective plugin resolver', () => { await expect(effective.resolve({ projectId: 'project-a', projectPath: 'C:/project-a', - assignedSkillIds: ['game-resource'], + assignedSkillIds: [], role: 'parent', })).resolves.toMatchObject({ pluginReleaseIds: [bundled.releaseId], @@ -504,6 +506,38 @@ describe('effective plugin resolver', () => { expect(getInstalledRelease).not.toHaveBeenCalled(); }); + it('materializes the system-included Data Service after project enablement without Agent assignment', async () => { + const policy = currentPolicy(); + policy.catalog.plugins[0] = { + ...policy.catalog.plugins[0], + plugin_id: DATA_SERVICE_PLUGIN_DEFINITION.id, + capabilities: DATA_SERVICE_PLUGIN_DEFINITION.operations.map(({ capabilityId, operation }) => ({ + capability_id: capabilityId, + operations: [{ + operation, + billing: { mode: 'included' as const, entitlement_scope: null, notice: 'Included' }, + }], + })), + }; + const effective = createEffectivePluginResolver({ + definitions: [DATA_SERVICE_PLUGIN_DEFINITION], + getAccountBinding: () => binding, + getEnabledPluginIds: vi.fn(async () => [DATA_SERVICE_PLUGIN_DEFINITION.id]), + policyClient: { getState: () => policy, refresh: vi.fn() }, + }); + + await expect(effective.resolve({ + projectId: 'project-a', + projectPath: 'C:/project-a', + assignedSkillIds: [], + role: 'parent', + })).resolves.toMatchObject({ + effectiveSkillIds: ['data-service'], + skillEntries: [{ id: 'data-service', entryPath: 'skills/data-service/SKILL.md' }], + unavailableReasons: [], + }); + }); + it('materializes the project-wide Scaffold Skill after project enablement without Agent assignment', async () => { const scaffoldLibrary = { ...library(), diff --git a/tests/unit/plugin-workspace-model.test.ts b/tests/unit/plugin-workspace-model.test.ts index 5335b09..33bac97 100644 --- a/tests/unit/plugin-workspace-model.test.ts +++ b/tests/unit/plugin-workspace-model.test.ts @@ -257,7 +257,7 @@ describe('buildPluginWorkspaceProjection', () => { channel: 'stable', }, }, - })).toEqual(['disable_project', 'open_agent_assignment', 'open_settings']); + })).toEqual(['disable_project', 'open_settings']); }); it('keeps bundled and downloadable official command sets inside their delivery boundaries', () => { @@ -298,9 +298,7 @@ describe('buildPluginWorkspaceProjection', () => { library: { ...library, items: [bundledLibrary] }, marketplaceInstallations: {}, project: bundledProject, - })).toEqual(pluginId === 'makelore.project-scaffold' - ? ['remove_from_library', 'enable_project'] - : ['remove_from_library', 'enable_project', 'open_agent_assignment']); + })).toEqual(['remove_from_library', 'enable_project']); } expect(commandKinds({ marketplaceInstallations: {} })).toEqual([ diff --git a/tests/unit/plugins-page.test.tsx b/tests/unit/plugins-page.test.tsx index 03a0661..f1c8440 100644 --- a/tests/unit/plugins-page.test.tsx +++ b/tests/unit/plugins-page.test.tsx @@ -89,7 +89,6 @@ const officialItem: PluginWorkspaceItem = { deviceReason: null, commands: [ { kind: 'disable_project', projectId: 'project-a', pluginId: 'makelore.data-service' }, - { kind: 'open_agent_assignment', projectId: 'project-a', pluginId: 'makelore.data-service' }, { kind: 'open_settings', projectId: 'project-a', pluginId: 'makelore.data-service' }, ], official: { @@ -433,7 +432,7 @@ describe('PluginsView', () => { expect(screen.getByRole('heading', { name: '本机全局生效' })).toBeVisible(); expect(screen.getByText('本机全局已启用')).toBeVisible(); expect(screen.getByText('随 MakeLore 提供')).toBeVisible(); - expect(screen.getByText('已分配 1 位伙伴')).toBeVisible(); + expect(screen.getByText('随项目启用')).toBeVisible(); expect(screen.getAllByRole('alert')).toHaveLength(2); expect(screen.queryByRole('textbox', { name: /包|路径|source/i })).not.toBeInTheDocument(); expect(screen.getByText(/通过对话安装/)).toBeVisible(); @@ -501,7 +500,10 @@ describe('PluginsView', () => { expect(dialog).toHaveTextContent('读取项目名称'); expect(dialog).toHaveTextContent('data-service.documents'); expect(dialog).toHaveTextContent('按平台包含'); - expect(dialog).toHaveTextContent('小明'); + expect(dialog).toHaveTextContent('生效范围'); + expect(dialog).toHaveTextContent('无需单独分配'); + expect(dialog).not.toHaveTextContent('小明'); + expect(screen.queryByRole('button', { name: '分配开发数据服务给伙伴' })).not.toBeInTheDocument(); expect(dialog).toHaveTextContent('插件专属设置'); expect(dialog).toHaveTextContent('创建开发数据空间'); @@ -727,9 +729,9 @@ describe('PluginsView', () => { const view = props(officialItem); render(); - fireEvent.click(screen.getByRole('button', { name: '分配开发数据服务给伙伴' })); + fireEvent.click(screen.getByRole('button', { name: '查看开发数据服务插件设置' })); expect(view.onCommand).toHaveBeenCalledWith({ - kind: 'open_agent_assignment', projectId: 'project-a', pluginId: 'makelore.data-service', + kind: 'open_settings', projectId: 'project-a', pluginId: 'makelore.data-service', }); fireEvent.click(screen.getByRole('button', { name: '关闭插件详情' })); expect(view.onSelect).toHaveBeenCalledWith(null);