feat: make project scaffold project-wide
This commit is contained in:
1 parent
d642d7607c
commit
6710527e8f
9 files changed
+252
-12
No files matched your search
@@ -0,0 +1,116 @@
|
|||||||
|
# Task: Make project-scoped Plugins active without partner assignment
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
|
||||||
|
- Task ID: 20260905-project-plugin-scope-7c4e9a21
|
||||||
|
- Mode: Feature
|
||||||
|
- Branch: codex/20260905-project-plugin-scope-7c4e9a21-project-plugin-scope
|
||||||
|
- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260905-project-plugin-scope-7c4e9a21
|
||||||
|
- Base commit: d642d7607c26dee01ef65b4e70dd756465dea16a
|
||||||
|
- Owner: codex-root
|
||||||
|
- Status: Ready for Integration
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Reproduce whether the code-owned bundled `makelore.project-scaffold` Skill is
|
||||||
|
currently excluded from a parent Agent when the Plugin is acquired and enabled for
|
||||||
|
the project but not explicitly assigned to that Agent.
|
||||||
|
- Change only Project Scaffold activation semantics so project enablement is sufficient
|
||||||
|
for parent-Agent materialization; preserve Account Library acquisition, child-worker
|
||||||
|
emptiness, frozen worker generations, and every other Plugin's assignment behavior.
|
||||||
|
- Remove Project Scaffold's partner-assignment action/status from the unified Plugins
|
||||||
|
workspace and add focused runtime plus Renderer regressions.
|
||||||
|
- Record the accepted user decision as an Integration promotion candidate; this Feature
|
||||||
|
task does not edit canonical architecture, domain, or ADR files.
|
||||||
|
|
||||||
|
## Intent And Constraints
|
||||||
|
|
||||||
|
- Concurrent Task Gate: Passed. The exact owner/worktree/branch/base identity was
|
||||||
|
verified through `task_context.py status --json`; related Plugin download/status
|
||||||
|
tasks are Ready for Integration and have no active writer overlapping this scope.
|
||||||
|
- Planning Gate: Passed after loading the project entry docs, current state, decision
|
||||||
|
index, ADR-008, architecture/data flow/module map, business rules, success criteria,
|
||||||
|
glossary, evidence, reflection, commitments, stale items, and relevant peer records.
|
||||||
|
- Project Context Loaded:
|
||||||
|
- Positioning: MakeLore Code is a Main-owned Pi runtime whose Plugin state is projected
|
||||||
|
through existing Marketplace/project/Agent authorities rather than Renderer-owned
|
||||||
|
lifecycle state.
|
||||||
|
- Current focus: official bundled Project Scaffold delivery and unified `/plugins`
|
||||||
|
presentation are already integrated; the remaining question is activation scope.
|
||||||
|
- Applicable decision: ADR-008 previously required acquisition, project enablement,
|
||||||
|
and Agent assignment. The user's explicit 2026-09-05 decision supersedes only the
|
||||||
|
assignment requirement for Project Scaffold.
|
||||||
|
- Architecture boundaries: effective resolution remains Main-owned; Renderer only
|
||||||
|
projects actions. Parent workers may receive the Skill, child workers remain empty,
|
||||||
|
and active workers keep frozen resources until replacement/settlement.
|
||||||
|
- Current state: `makelore.project-scaffold` ships in the signed client and has no
|
||||||
|
device download path; Account acquisition and project enablement remain distinct.
|
||||||
|
- Known risk: hiding the UI assignment action without changing the effective resolver
|
||||||
|
would leave the Skill unusable; broadening all Plugin Skills would silently alter
|
||||||
|
unrelated assignment semantics.
|
||||||
|
- Relevant commitment: final packaged Project Scaffold activation still requires a
|
||||||
|
rebuilt client and installed-client smoke; workspace tests do not prove that step.
|
||||||
|
- Relevant peers: the bundled-status integration and device-action-gap tasks only
|
||||||
|
correct delivery presentation/package evidence; neither changes activation scope.
|
||||||
|
- TDD boundary: first assert the public effective-resolver and Plugins-page behavior at
|
||||||
|
acquired + project-enabled + unassigned state, then implement the narrowest shared
|
||||||
|
activation-scope rule that makes those assertions pass.
|
||||||
|
- Do not modify the occupied client root, Server, Marketplace contracts, Package Store,
|
||||||
|
billing, hosted execution, local Device Packages, or unrelated Plugin assignments.
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
- Confirmed the defect at the Main-owned effective-resolver seam: an acquired,
|
||||||
|
project-enabled Project Scaffold Plugin with no Agent assignment was rejected as
|
||||||
|
`skill_unassigned`, so removing only the Renderer action would not have activated it.
|
||||||
|
- Added one narrow code-owned project-wide activation invariant for
|
||||||
|
`makelore.project-scaffold`. Its full Skill set now materializes for every parent Agent
|
||||||
|
after Account acquisition and project enablement; disabled projects remain blocked and
|
||||||
|
child Agents remain empty.
|
||||||
|
- Updated the unified Plugins workspace so Project Scaffold no longer offers partner
|
||||||
|
assignment. Cards and details now describe its scope as `随项目启用` / `生效范围`.
|
||||||
|
- Preserved existing assignment behavior for Game Resource, Data Service, Marketplace,
|
||||||
|
and local Plugins. No manifest schema, server contract, Package Store, billing, or
|
||||||
|
worker-lifecycle authority was added.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- TDD RED: the focused resolver/model/page slice produced 3 expected failures and 42
|
||||||
|
passes: Project Scaffold returned `skill_unassigned`, the assignment command remained,
|
||||||
|
and the project-wide scope copy was absent.
|
||||||
|
- Focused GREEN: 3 files / 45 tests passed.
|
||||||
|
- Adjacent Plugin/runtime regression: 12 files / 87 tests passed, including effective
|
||||||
|
resolution, composition, lifecycle, manifest/routes, Pi resource/worker opening,
|
||||||
|
Project Scaffold, and unified Plugins projection/controller/query/page behavior.
|
||||||
|
- `corepack pnpm run typecheck`: passed.
|
||||||
|
- Scoped ESLint: passed. Full `corepack pnpm run lint:check`: 0 errors and 5 unchanged
|
||||||
|
warnings in untouched Home/Makelore files.
|
||||||
|
- Full regular unit suite: 225 files passed and 1 unrelated real-process timing test
|
||||||
|
failed (`pi-agent-server-process-real`, 2630 ms against a 2000 ms threshold); 1888 tests
|
||||||
|
passed and 2 skipped. The exact failed file then passed in isolation, 6/6.
|
||||||
|
- Isolated pressure suite: 1/1 passed.
|
||||||
|
- `corepack pnpm run build:vite`: Renderer, Main, Preload, and utility builds passed with
|
||||||
|
existing Browserslist/import/chunk warnings.
|
||||||
|
- `git diff --check`: passed.
|
||||||
|
|
||||||
|
## Follow-ups
|
||||||
|
|
||||||
|
- The Integration task must promote the Project Scaffold activation rule into ADR-008,
|
||||||
|
module map, data flow, and business rules, then rebuild/install the client and smoke-test
|
||||||
|
an acquired + project-enabled + unassigned parent Agent. Workspace tests do not prove
|
||||||
|
the installed-client generation was replaced.
|
||||||
|
|
||||||
|
## Promotion Candidates
|
||||||
|
|
||||||
|
- Update ADR-008, module map, data flow, and business rules during Integration so
|
||||||
|
`makelore.project-scaffold` is documented as account-acquired + project-enabled and
|
||||||
|
automatically available to parent Agents, with no partner-assignment state.
|
||||||
|
- Evidence: focused resolver RED reproduced `skill_unassigned`; the corrected focused
|
||||||
|
and adjacent suites pass while Game Resource retains `open_agent_assignment`.
|
||||||
|
- Future impact: Plugin activation-scope changes must update the code-owned scope
|
||||||
|
predicate and both Main/Renderer regressions; Project Scaffold assignments already
|
||||||
|
stored in project data become inert but may remain preserved.
|
||||||
|
- Semantic conflict: supersedes only ADR-008's assignment requirement for Project
|
||||||
|
Scaffold, not the general assignment model for other Plugins.
|
||||||
|
- Human confirmation required: No; the user explicitly chose project-level activation
|
||||||
|
on 2026-09-05.
|
||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
CodingPluginDefinition,
|
CodingPluginDefinition,
|
||||||
CodingPluginToolDefinition,
|
CodingPluginToolDefinition,
|
||||||
} from '../../shared/coding-plugins';
|
} from '../../shared/coding-plugins';
|
||||||
|
import { isProjectWideCodingPluginId } from '../../shared/coding-plugins';
|
||||||
import {
|
import {
|
||||||
CORE_CODING_SKILL_IDS,
|
CORE_CODING_SKILL_IDS,
|
||||||
type CodingSkillId,
|
type CodingSkillId,
|
||||||
@@ -240,6 +241,15 @@ function marketplaceSkillConflicts(records: readonly DefinitionRecord[]): Readon
|
|||||||
return blocked;
|
return blocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectedSkillsForDefinition(
|
||||||
|
definition: CodingPluginDefinition,
|
||||||
|
assignedSkillIds: readonly string[],
|
||||||
|
): CodingPluginDefinition['skills'] {
|
||||||
|
return isProjectWideCodingPluginId(definition.id)
|
||||||
|
? definition.skills
|
||||||
|
: definition.skills.filter(({ id }) => assignedSkillIds.includes(id));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a worker snapshot from separated Marketplace, Package Store,
|
* Resolve a worker snapshot from separated Marketplace, Package Store,
|
||||||
* project, assignment, and policy state. This module deliberately performs
|
* project, assignment, and policy state. This module deliberately performs
|
||||||
@@ -303,7 +313,7 @@ export class EffectivePluginResolver {
|
|||||||
const enabled = new Set(await this.enabledPluginIds(input.projectPath));
|
const enabled = new Set(await this.enabledPluginIds(input.projectPath));
|
||||||
const requiresLibrary = userDefinitions.some(({ definition, installed }) => (
|
const requiresLibrary = userDefinitions.some(({ definition, installed }) => (
|
||||||
installed && enabled.has(definition.id)
|
installed && enabled.has(definition.id)
|
||||||
&& definition.skills.some(({ id }) => assigned.includes(id))
|
&& selectedSkillsForDefinition(definition, assigned).length > 0
|
||||||
));
|
));
|
||||||
const library = input.library !== undefined
|
const library = input.library !== undefined
|
||||||
? input.library
|
? input.library
|
||||||
@@ -314,7 +324,8 @@ export class EffectivePluginResolver {
|
|||||||
const serverDefinitions = definitions.filter(({ definition }) => definition.requiresBackend);
|
const serverDefinitions = definitions.filter(({ definition }) => definition.requiresBackend);
|
||||||
const needsPolicyRefresh = policyState.status !== 'current'
|
const needsPolicyRefresh = policyState.status !== 'current'
|
||||||
&& serverDefinitions.some(({ definition, installed }) => (
|
&& serverDefinitions.some(({ definition, installed }) => (
|
||||||
installed && enabled.has(definition.id) && definition.skills.some(({ id }) => assigned.includes(id))
|
installed && enabled.has(definition.id)
|
||||||
|
&& selectedSkillsForDefinition(definition, assigned).length > 0
|
||||||
));
|
));
|
||||||
if (needsPolicyRefresh && this.options.policyClient) {
|
if (needsPolicyRefresh && this.options.policyClient) {
|
||||||
await this.options.policyClient.refresh();
|
await this.options.policyClient.refresh();
|
||||||
@@ -330,7 +341,7 @@ export class EffectivePluginResolver {
|
|||||||
));
|
));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const selectedSkills = definition.skills.filter(({ id }) => assigned.includes(id));
|
const selectedSkills = selectedSkillsForDefinition(definition, assigned);
|
||||||
if (selectedSkills.length === 0) {
|
if (selectedSkills.length === 0) {
|
||||||
unavailableReasons.push(unavailable(definition.id, 'skill_unassigned', 'Plugin Skill is not assigned'));
|
unavailableReasons.push(unavailable(definition.id, 'skill_unassigned', 'Plugin Skill is not assigned'));
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -106,6 +106,15 @@ export const GAME_RESOURCE_BUNDLED_RELEASE_ID = '00000000-0000-4000-8000-0000000
|
|||||||
export const PROJECT_SCAFFOLD_PLUGIN_ID = 'makelore.project-scaffold' as const;
|
export const PROJECT_SCAFFOLD_PLUGIN_ID = 'makelore.project-scaffold' as const;
|
||||||
export const PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID = '00000000-0000-4000-8000-000000000303' as const;
|
export const PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID = '00000000-0000-4000-8000-000000000303' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project-wide Plugins materialize all of their Skills for every parent Agent
|
||||||
|
* once the Plugin is enabled for that project. They never require a separate
|
||||||
|
* Agent Skill assignment.
|
||||||
|
*/
|
||||||
|
export function isProjectWideCodingPluginId(pluginId: string): boolean {
|
||||||
|
return pluginId === PROJECT_SCAFFOLD_PLUGIN_ID;
|
||||||
|
}
|
||||||
|
|
||||||
export const CODE_OWNED_OPTIONAL_BUNDLED_RELEASES = Object.freeze({
|
export const CODE_OWNED_OPTIONAL_BUNDLED_RELEASES = Object.freeze({
|
||||||
[GAME_RESOURCE_PLUGIN_ID]: Object.freeze({
|
[GAME_RESOURCE_PLUGIN_ID]: Object.freeze({
|
||||||
releaseId: GAME_RESOURCE_BUNDLED_RELEASE_ID,
|
releaseId: GAME_RESOURCE_BUNDLED_RELEASE_ID,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import type { DataServiceInstanceState } from '../../../shared/data-service';
|
import type { DataServiceInstanceState } from '../../../shared/data-service';
|
||||||
import {
|
import {
|
||||||
isBundledOfficialPlugin,
|
isBundledOfficialPlugin,
|
||||||
|
isProjectWideOfficialPlugin,
|
||||||
type PluginWorkspaceCommand,
|
type PluginWorkspaceCommand,
|
||||||
type PluginWorkspaceItem,
|
type PluginWorkspaceItem,
|
||||||
} from './plugin-workspace-model';
|
} from './plugin-workspace-model';
|
||||||
@@ -402,8 +403,12 @@ export function PluginDetails(props: PluginDetailsProps) {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section aria-labelledby="plugin-agents-heading">
|
<section aria-labelledby="plugin-agents-heading">
|
||||||
<h3 id="plugin-agents-heading" className="text-lg font-semibold">伙伴分配</h3>
|
<h3 id="plugin-agents-heading" className="text-lg font-semibold">
|
||||||
{props.item.source === 'local'
|
{isProjectWideOfficialPlugin(props.item) ? '生效范围' : '伙伴分配'}
|
||||||
|
</h3>
|
||||||
|
{isProjectWideOfficialPlugin(props.item)
|
||||||
|
? <p className="mt-2 text-pretty text-sm text-muted-foreground">启用当前项目后,插件会自动提供给该项目的主伙伴,无需单独分配;子伙伴不会继承。</p>
|
||||||
|
: props.item.source === 'local'
|
||||||
? <p className="mt-2 text-pretty text-sm text-muted-foreground">本机包全局生效,不作为项目 Agent Skill 分配项;符合条件的新建或空闲主伙伴及其 worker 会按既有生命周期加载。</p>
|
? <p className="mt-2 text-pretty text-sm text-muted-foreground">本机包全局生效,不作为项目 Agent Skill 分配项;符合条件的新建或空闲主伙伴及其 worker 会按既有生命周期加载。</p>
|
||||||
: props.item.assignedAgentNames.length
|
: props.item.assignedAgentNames.length
|
||||||
? <div className="mt-3 flex flex-wrap gap-2">{props.item.assignedAgentNames.map((name) => <Badge key={name} variant="secondary">{name}</Badge>)}</div>
|
? <div className="mt-3 flex flex-wrap gap-2">{props.item.assignedAgentNames.map((name) => <Badge key={name} variant="secondary">{name}</Badge>)}</div>
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ import type {
|
|||||||
PluginWorkspaceProjection,
|
PluginWorkspaceProjection,
|
||||||
PluginWorkspaceState,
|
PluginWorkspaceState,
|
||||||
} from './plugin-workspace-model';
|
} from './plugin-workspace-model';
|
||||||
import { buildPluginWorkspaceProjection, isBundledOfficialPlugin } from './plugin-workspace-model';
|
import {
|
||||||
|
buildPluginWorkspaceProjection,
|
||||||
|
isBundledOfficialPlugin,
|
||||||
|
isProjectWideOfficialPlugin,
|
||||||
|
} from './plugin-workspace-model';
|
||||||
import { resolvePluginWorkspaceSearch, serializePluginWorkspaceSearch } from './plugin-workspace-query';
|
import { resolvePluginWorkspaceSearch, serializePluginWorkspaceSearch } from './plugin-workspace-query';
|
||||||
|
|
||||||
const DELIVERY_TEXT = {
|
const DELIVERY_TEXT = {
|
||||||
@@ -82,10 +86,15 @@ function cardSource(item: PluginWorkspaceItem): string {
|
|||||||
|
|
||||||
function agentText(item: PluginWorkspaceItem): string {
|
function agentText(item: PluginWorkspaceItem): string {
|
||||||
if (item.source === 'local') return '本机全局生效';
|
if (item.source === 'local') return '本机全局生效';
|
||||||
|
if (isProjectWideOfficialPlugin(item)) return '随项目启用';
|
||||||
const count = item.assignedAgentIds.length;
|
const count = item.assignedAgentIds.length;
|
||||||
return count ? `已分配 ${count} 位伙伴` : '尚未分配伙伴';
|
return count ? `已分配 ${count} 位伙伴` : '尚未分配伙伴';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function agentLabel(item: PluginWorkspaceItem): string {
|
||||||
|
return isProjectWideOfficialPlugin(item) ? '生效范围' : '伙伴';
|
||||||
|
}
|
||||||
|
|
||||||
function PluginCard({
|
function PluginCard({
|
||||||
item,
|
item,
|
||||||
grouped = false,
|
grouped = false,
|
||||||
@@ -112,7 +121,7 @@ function PluginCard({
|
|||||||
<dl className="mt-4 space-y-2 text-sm">
|
<dl className="mt-4 space-y-2 text-sm">
|
||||||
<div className="flex justify-between gap-4"><dt className="text-muted-foreground">当前项目</dt><dd className="text-right font-medium">{PROJECT_TEXT[item.projectState]}</dd></div>
|
<div className="flex justify-between gap-4"><dt className="text-muted-foreground">当前项目</dt><dd className="text-right font-medium">{PROJECT_TEXT[item.projectState]}</dd></div>
|
||||||
<div className="flex justify-between gap-4"><dt className="text-muted-foreground">本机状态</dt><dd className="text-right font-medium">{item.source === 'local' ? (item.localEnabled ? '本机全局已启用' : '本机全局已停用') : item.official?.installation?.version ? `官方包 ${item.official.installation.version}` : isBundledOfficialPlugin(item) ? '随应用提供' : '未下载官方包'}</dd></div>
|
<div className="flex justify-between gap-4"><dt className="text-muted-foreground">本机状态</dt><dd className="text-right font-medium">{item.source === 'local' ? (item.localEnabled ? '本机全局已启用' : '本机全局已停用') : item.official?.installation?.version ? `官方包 ${item.official.installation.version}` : isBundledOfficialPlugin(item) ? '随应用提供' : '未下载官方包'}</dd></div>
|
||||||
<div className="flex justify-between gap-4"><dt className="text-muted-foreground">伙伴</dt><dd className="text-right font-medium">{agentText(item)}</dd></div>
|
<div className="flex justify-between gap-4"><dt className="text-muted-foreground">{agentLabel(item)}</dt><dd className="text-right font-medium">{agentText(item)}</dd></div>
|
||||||
<div className="flex justify-between gap-4"><dt className="text-muted-foreground">计费</dt><dd className="text-right font-medium">{BILLING_TEXT[item.billing]}</dd></div>
|
<div className="flex justify-between gap-4"><dt className="text-muted-foreground">计费</dt><dd className="text-right font-medium">{BILLING_TEXT[item.billing]}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
{item.deviceReason ? (
|
{item.deviceReason ? (
|
||||||
@@ -182,7 +191,7 @@ export function PluginsView(props: PluginsViewProps) {
|
|||||||
<p className="text-sm font-medium text-brand">MakeLore Code</p>
|
<p className="text-sm font-medium text-brand">MakeLore Code</p>
|
||||||
<h1 className="mt-1 text-balance text-3xl font-semibold tracking-[-0.03em]">插件</h1>
|
<h1 className="mt-1 text-balance text-3xl font-semibold tracking-[-0.03em]">插件</h1>
|
||||||
<p className="mt-2 max-w-3xl text-pretty text-sm leading-6 text-muted-foreground">
|
<p className="mt-2 max-w-3xl text-pretty text-sm leading-6 text-muted-foreground">
|
||||||
MakeLore 运营发布官方插件;本机 Skill 和 Pi extension 仅通过对话安装。获取、项目启用与伙伴分配彼此独立,本页集中展示账号、设备、当前项目与伙伴状态。
|
MakeLore 运营发布官方插件;本机 Skill 和 Pi extension 仅通过对话安装。获取与项目启用彼此独立;需要定向生效的插件还可分配伙伴。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button type="button" variant="outline" className="min-h-10" onClick={() => void props.onRefresh()}>
|
<Button type="button" variant="outline" className="min-h-10" onClick={() => void props.onRefresh()}>
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import type {
|
|||||||
MarketplaceUsageBilling,
|
MarketplaceUsageBilling,
|
||||||
} from '@/lib/plugin-marketplace';
|
} from '@/lib/plugin-marketplace';
|
||||||
import type { DevicePackageIndexV1, DevicePackageRecordV1 } from '../../../shared/device-packages';
|
import type { DevicePackageIndexV1, DevicePackageRecordV1 } from '../../../shared/device-packages';
|
||||||
import { isCodeOwnedOptionalBundledPluginId } from '../../../shared/coding-plugins';
|
import {
|
||||||
|
isCodeOwnedOptionalBundledPluginId,
|
||||||
|
isProjectWideCodingPluginId,
|
||||||
|
} from '../../../shared/coding-plugins';
|
||||||
|
|
||||||
export type PluginWorkspaceScope = 'all' | 'project';
|
export type PluginWorkspaceScope = 'all' | 'project';
|
||||||
export type PluginWorkspaceSource = 'all' | 'official' | 'local';
|
export type PluginWorkspaceSource = 'all' | 'official' | 'local';
|
||||||
@@ -109,6 +112,10 @@ export function isBundledOfficialPlugin(item: PluginWorkspaceItem): boolean {
|
|||||||
&& (item.delivery === 'system_included' || isCodeOwnedOptionalBundledPluginId(item.pluginId));
|
&& (item.delivery === 'system_included' || isCodeOwnedOptionalBundledPluginId(item.pluginId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isProjectWideOfficialPlugin(item: PluginWorkspaceItem): boolean {
|
||||||
|
return item.source === 'official' && isProjectWideCodingPluginId(item.pluginId);
|
||||||
|
}
|
||||||
|
|
||||||
export type PluginWorkspaceNotice =
|
export type PluginWorkspaceNotice =
|
||||||
| { kind: 'scope_fallback'; message: string }
|
| { kind: 'scope_fallback'; message: string }
|
||||||
| { kind: 'source_unavailable'; source: 'catalog' | 'library' | 'device' | 'project'; message: string }
|
| { kind: 'source_unavailable'; source: 'catalog' | 'library' | 'device' | 'project'; message: string }
|
||||||
@@ -257,7 +264,7 @@ function officialCommands(
|
|||||||
} else if (deliveryReady && sourceAvailable && record.project.state !== 'unavailable') {
|
} else if (deliveryReady && sourceAvailable && record.project.state !== 'unavailable') {
|
||||||
commands.push({ kind: 'enable_project', projectId: input.activeProject.id, pluginId: record.pluginId });
|
commands.push({ kind: 'enable_project', projectId: input.activeProject.id, pluginId: record.pluginId });
|
||||||
}
|
}
|
||||||
if (deliveryReady && sourceAvailable) {
|
if (deliveryReady && sourceAvailable && !isProjectWideCodingPluginId(record.pluginId)) {
|
||||||
commands.push({ kind: 'open_agent_assignment', projectId: input.activeProject.id, pluginId: record.pluginId });
|
commands.push({ kind: 'open_agent_assignment', projectId: input.activeProject.id, pluginId: record.pluginId });
|
||||||
}
|
}
|
||||||
if (record.project.enabled && deliveryReady && sourceAvailable && record.project.settingsSurface) {
|
if (record.project.enabled && deliveryReady && sourceAvailable && record.project.settingsSurface) {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
} from '../../electron/coding-plugins/effective-resolver';
|
} from '../../electron/coding-plugins/effective-resolver';
|
||||||
import {
|
import {
|
||||||
DATA_SERVICE_PLUGIN_DEFINITION,
|
DATA_SERVICE_PLUGIN_DEFINITION,
|
||||||
|
PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID,
|
||||||
|
PROJECT_SCAFFOLD_PLUGIN_ID,
|
||||||
type CodingPluginDefinition,
|
type CodingPluginDefinition,
|
||||||
} from '../../shared/coding-plugins';
|
} from '../../shared/coding-plugins';
|
||||||
|
|
||||||
@@ -53,6 +55,20 @@ const serverDefinition: CodingPluginDefinition = {
|
|||||||
surfaces: {},
|
surfaces: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const projectScaffoldDefinition: CodingPluginDefinition = {
|
||||||
|
...skillOnlyDefinition,
|
||||||
|
id: PROJECT_SCAFFOLD_PLUGIN_ID,
|
||||||
|
displayName: 'Project Scaffold',
|
||||||
|
description: 'Project-scoped scaffold Skill',
|
||||||
|
releaseId: PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID,
|
||||||
|
provenance: { source: 'bundled', packageRoot: 'project-scaffold' },
|
||||||
|
skills: [{
|
||||||
|
id: 'makelore-project-scaffold',
|
||||||
|
entryPath: 'skills/makelore-project-scaffold/SKILL.md',
|
||||||
|
grants: [],
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
const binding = { accountKey: 'account-a', epoch: 4 };
|
const binding = { accountKey: 'account-a', epoch: 4 };
|
||||||
|
|
||||||
function library(runtimeStatus: 'enabled' | 'suspended' = 'enabled', catalogStatus: 'active' | 'retired' = 'active') {
|
function library(runtimeStatus: 'enabled' | 'suspended' = 'enabled', catalogStatus: 'active' | 'retired' = 'active') {
|
||||||
@@ -487,4 +503,65 @@ describe('effective plugin resolver', () => {
|
|||||||
.resolves.toMatchObject({ id: bundled.id, releaseId: bundled.releaseId });
|
.resolves.toMatchObject({ id: bundled.id, releaseId: bundled.releaseId });
|
||||||
expect(getInstalledRelease).not.toHaveBeenCalled();
|
expect(getInstalledRelease).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('materializes the project-wide Scaffold Skill after project enablement without Agent assignment', async () => {
|
||||||
|
const scaffoldLibrary = {
|
||||||
|
...library(),
|
||||||
|
items: [{
|
||||||
|
...library().items[0],
|
||||||
|
pluginId: PROJECT_SCAFFOLD_PLUGIN_ID,
|
||||||
|
title: projectScaffoldDefinition.displayName,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
const effective = createEffectivePluginResolver({
|
||||||
|
definitions: [projectScaffoldDefinition],
|
||||||
|
getAccountBinding: () => binding,
|
||||||
|
getLibrary: vi.fn(async () => scaffoldLibrary),
|
||||||
|
getEnabledPluginIds: vi.fn(async () => [PROJECT_SCAFFOLD_PLUGIN_ID]),
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(effective.resolve({
|
||||||
|
projectId: 'project-a',
|
||||||
|
projectPath: 'C:/project-a',
|
||||||
|
assignedSkillIds: [],
|
||||||
|
role: 'parent',
|
||||||
|
})).resolves.toMatchObject({
|
||||||
|
pluginReleaseIds: [PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID],
|
||||||
|
effectiveSkillIds: ['makelore-project-scaffold'],
|
||||||
|
skillEntries: [{
|
||||||
|
id: 'makelore-project-scaffold',
|
||||||
|
entryPath: 'skills/makelore-project-scaffold/SKILL.md',
|
||||||
|
}],
|
||||||
|
unavailableReasons: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(createEffectivePluginResolver({
|
||||||
|
definitions: [projectScaffoldDefinition],
|
||||||
|
getAccountBinding: () => binding,
|
||||||
|
getLibrary: vi.fn(async () => scaffoldLibrary),
|
||||||
|
getEnabledPluginIds: vi.fn(async () => []),
|
||||||
|
}).resolve({
|
||||||
|
projectId: 'project-a',
|
||||||
|
projectPath: 'C:/project-a',
|
||||||
|
assignedSkillIds: [],
|
||||||
|
role: 'parent',
|
||||||
|
})).resolves.toMatchObject({
|
||||||
|
effectiveSkillIds: [],
|
||||||
|
unavailableReasons: [expect.objectContaining({
|
||||||
|
pluginId: PROJECT_SCAFFOLD_PLUGIN_ID,
|
||||||
|
code: 'project_disabled',
|
||||||
|
})],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(effective.resolve({
|
||||||
|
projectId: 'project-a',
|
||||||
|
projectPath: 'C:/project-a',
|
||||||
|
assignedSkillIds: [],
|
||||||
|
role: 'child',
|
||||||
|
})).resolves.toMatchObject({
|
||||||
|
pluginReleaseIds: [],
|
||||||
|
effectiveSkillIds: [],
|
||||||
|
skillEntries: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -298,7 +298,9 @@ describe('buildPluginWorkspaceProjection', () => {
|
|||||||
library: { ...library, items: [bundledLibrary] },
|
library: { ...library, items: [bundledLibrary] },
|
||||||
marketplaceInstallations: {},
|
marketplaceInstallations: {},
|
||||||
project: bundledProject,
|
project: bundledProject,
|
||||||
})).toEqual(['remove_from_library', 'enable_project', 'open_agent_assignment']);
|
})).toEqual(pluginId === 'makelore.project-scaffold'
|
||||||
|
? ['remove_from_library', 'enable_project']
|
||||||
|
: ['remove_from_library', 'enable_project', 'open_agent_assignment']);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(commandKinds({ marketplaceInstallations: {} })).toEqual([
|
expect(commandKinds({ marketplaceInstallations: {} })).toEqual([
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ describe('PluginsView', () => {
|
|||||||
|
|
||||||
expect(screen.getByRole('heading', { name: '插件' })).toBeVisible();
|
expect(screen.getByRole('heading', { name: '插件' })).toBeVisible();
|
||||||
expect(screen.getByText(/MakeLore 运营发布官方插件/)).toBeVisible();
|
expect(screen.getByText(/MakeLore 运营发布官方插件/)).toBeVisible();
|
||||||
expect(screen.getByText(/获取、项目启用与伙伴分配彼此独立/)).toBeVisible();
|
expect(screen.getByText(/获取与项目启用彼此独立.*需要定向生效的插件还可分配伙伴/)).toBeVisible();
|
||||||
expect(screen.getByText('开发数据服务')).toBeVisible();
|
expect(screen.getByText('开发数据服务')).toBeVisible();
|
||||||
expect(screen.getByText('Pi Web Search')).toBeVisible();
|
expect(screen.getByText('Pi Web Search')).toBeVisible();
|
||||||
expect(screen.getByRole('heading', { name: '本机全局生效' })).toBeVisible();
|
expect(screen.getByRole('heading', { name: '本机全局生效' })).toBeVisible();
|
||||||
@@ -486,6 +486,10 @@ describe('PluginsView', () => {
|
|||||||
|
|
||||||
expect(screen.getAllByText('随应用提供')).toHaveLength(2);
|
expect(screen.getAllByText('随应用提供')).toHaveLength(2);
|
||||||
expect(screen.queryByText('未下载官方包')).not.toBeInTheDocument();
|
expect(screen.queryByText('未下载官方包')).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByText('随项目启用')).toBeVisible();
|
||||||
|
expect(screen.getByRole('heading', { name: '生效范围' })).toBeVisible();
|
||||||
|
expect(screen.getByText(/启用当前项目后.*无需单独分配/)).toBeVisible();
|
||||||
|
expect(screen.queryByRole('heading', { name: '伙伴分配' })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows one dialog detail surface, confirms destructive commands, and embeds Data Service settings', () => {
|
it('shows one dialog detail surface, confirms destructive commands, and embeds Data Service settings', () => {
|
||||||
|
|||||||
Reference in new issue
Block a user