Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions docs/ai/design/2026-09-05-feature-builtin-remote.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
phase: design
title: Remote Built-in Skills Manifest Design
description: Load and validate the live curated skill list with a safe process-local fallback
---

# Remote Built-in Skills Manifest Design

## Architecture Overview

```mermaid
flowchart LR
F[Built-in flow] --> L[getBuiltinSkillNames]
L -->|first call| R[raw GitHub main manifest]
L --> V[validate bare array]
V -->|valid| N[readonly string list]
R -->|network/HTTP/JSON failure| B[embedded 21-name fallback]
V -->|invalid| B
N --> C[init / skill / setup / status]
B --> C
L -->|later calls| P[shared process promise]
```

`packages/cli/src/lib/BuiltinSkills.ts` is the only fetch, parse, validation, fallback, and promise-cache boundary. Consumers remain responsible only for using the resolved names.

## Data Model

`skills/built-in.json` is a bare JSON array:

```json
["agent-communication", "agent-management"]
```

The internal API is:

```ts
getBuiltinSkillNames(): Promise<readonly string[]>
```

Runtime names are strings. The unused compile-time `BuiltinSkillName` union is deleted because remote data cannot truthfully define a literal union.

## Component Breakdown

- `BuiltinSkills.ts` owns the raw `main` URL, trusted registry identifier, embedded fallback, validation, warning, and process-local promise.
- `init.ts` resolves built-ins only when the existing flow elects to install them, then maps names to template entries.
- `skill.ts` resolves names before the `--built-in` loop.
- `setup.service.ts` resolves names in the default installer; the cache prevents repeated requests across agents.
- `status.service.ts` resolves names before readiness checks; counts describe the selected live or fallback set.
- `SkillManager` remains unchanged and resolves `skills/<name>/SKILL.md` from the refreshed repository.

## Failure Contract

The loader treats a non-OK response, invalid JSON, or invalid manifest as one failure class: emit a warning containing the reason and return the embedded fallback. Consumers do not branch on source or fail setup.

Validation requires a non-empty array whose elements are non-empty, unique strings accepted by the existing skill-name rules. Validation is all-or-nothing.

## Design Decisions

- Fetch from `main` so maintainers can add built-ins without a CLI release.
- Use a bare array because there is no current caller for metadata or schema fields.
- Use a process-local promise cache, not persistent storage, because one stable result per invocation is sufficient.
- Keep the fallback beside the loader so there is one compiled list and no consumer duplication.
- Keep the registry identifier compiled because the remote manifest controls membership, not installation origin.
- Do not change `SkillManager`; its existing runtime path lookup already supports skills unknown to the compiled CLI.

## Non-Functional Requirements

- No built-in flow may contact the manifest more than once per process.
- Setup and status remain usable when GitHub is unavailable.
- Untrusted remote data is validated before it controls installation paths.
- Tests replace global `fetch` and never rely on network state.
64 changes: 64 additions & 0 deletions docs/ai/implementation/2026-09-05-feature-builtin-remote.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
phase: implementation
title: Remote Built-in Skills Manifest Implementation
description: Track implementation decisions, files, and design alignment
---

# Remote Built-in Skills Manifest Implementation

## Development Setup

- Worktree: `.worktrees/feature-builtin-remote`
- Branch: `feature-builtin-remote`, created from latest `origin/main`
- Bootstrap: `npm ci` and the six-project `npm run build`
- Method: TDD for each loader behavior and consumer migration

## Planned Code Structure

- `skills/built-in.json`: live bare-array manifest.
- `packages/cli/src/lib/BuiltinSkills.ts`: remote boundary, validation, promise cache, fallback, registry identity.
- Four existing consumers: asynchronous runtime list resolution.
- Focused loader and consumer tests: mocked fetch and fixture lists.

## Implementation Notes

- Added the live bare-array manifest with the 21 names present on the latest base.
- Added a single loader that shares its promise, validates remote data, and warns before returning the embedded fallback.
- Migrated init, skill add, setup, and status to await runtime names.
- Deleted the obsolete constants module and literal union.
- Left `SkillManager` unchanged because it already resolves validated runtime names from `skills/<name>/SKILL.md`.

## Error Handling

All remote failures warn once and return the embedded fallback. Invalid manifests are rejected wholesale.

## Security Notes

The remote list controls installation membership. The loader validates the full array at the network boundary before exposing names internally.

## Progress

- [x] Loader and manifest
- [x] Consumer migration
- [x] Implementation alignment check
- [x] Full validation

## Verification Evidence

- Focused suite: 7 files, 97 tests passed.
- CLI build: TypeScript declarations and 221 source files compiled successfully.
- Full build: all 6 projects passed.
- Full unit suite: all 6 projects passed serially, 2177 tests total.
- Full lint: all 6 projects passed.
- E2E: 41 tests passed.
- Optional task tracing was unavailable: `npx ai-devkit@latest task list --name builtin-remote --json` returned `unknown command 'task'`.

## Design Alignment

The implementation matches the approved single-loader data flow, all-or-nothing validation, process-local promise caching, embedded fallback, runtime string typing, and four consumer boundaries. No design deviations or follow-up work were identified.

## Publication

- Branch: `feature-builtin-remote`
- Pull request: https://github.com/codeaholicguy/ai-devkit/pull/214
- Merge intentionally left to reviewers.
45 changes: 45 additions & 0 deletions docs/ai/planning/2026-09-05-feature-builtin-remote.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
phase: planning
title: Remote Built-in Skills Manifest Plan
description: Ordered TDD tasks for the manifest loader and four consumers
---

# Remote Built-in Skills Manifest Plan

## Milestones

- [x] Milestone 1: Manifest loader is specified and implemented with fallback semantics.
- [x] Milestone 2: All four built-in consumers use runtime names.
- [x] Milestone 3: Documentation and full verification gates are complete.

## Task Breakdown

### Phase 1: Manifest and loader

- [x] Task 1.1: Add loader tests for a valid bare array and one-fetch promise caching. Evidence: focused loader test fails before implementation and passes afterward.
- [x] Task 1.2: Add loader tests for HTTP, parse, and invalid-shape fallback behavior, including empty, blank, duplicate, and invalid names. Dependency: Task 1.1. Evidence: focused tests.
- [x] Task 1.3: Add `skills/built-in.json` with the current 21 names from the latest base and implement the minimal loader. Dependency: failing tests. Evidence: focused tests and manifest parsing.

### Phase 2: Consumer migration

- [x] Task 2.1: Migrate `skill add --built-in` and setup using mocked runtime lists. Dependency: loader API. Evidence: focused command/service tests.
- [x] Task 2.2: Migrate init so the list is resolved only for a triggered built-in flow. Dependency: loader API. Evidence: focused init tests, including no-fetch skip behavior.
- [x] Task 2.3: Migrate status to report fixture-driven live counts and fallback warnings. Dependency: loader API. Evidence: focused status tests.
- [x] Task 2.4: Delete the compiled primary list and unused literal union; confirm no remaining references. Dependency: all consumers migrated. Evidence: `rg` and build.

### Phase 3: Verification and publication

- [x] Task 3.1: Reconcile implementation/testing docs and perform design-alignment review. Evidence: feature lint and diff review.
- [x] Task 3.2: Run `npm run build`, `npm test`, `npm run lint`, and E2E; fix regressions. Evidence: fresh exit-zero output.
- [x] Task 3.3: Create logical commits, rebase onto latest `origin/main`, rerun gates, push, and open the PR. Evidence: clean branch and PR URL.

## Dependencies and Risks

- Remote `main` may list a skill before its directory is available. Maintainers should land the directory and manifest atomically; `SkillManager` gives a clear not-found error.
- A rejected manifest uses the embedded fallback as one complete set; partial remote data is never installed.
- Existing tests with literal `20/20` output must use injected fixture counts where they test rendering rather than policy.
- All test suites must mock manifest fetches to preserve determinism.

## Progress Summary

The loader, manifest, and four consumers are complete and aligned with the approved design. The latest base added `ai-devkit-setup` after the original brief, so the manifest and safe fallback preserve the current 21-name set rather than regressing offline setup. Focused tests, the six-project build/lint/unit gates, E2E, and feature-doc lint pass. The branch is published for review in PR #214.
62 changes: 62 additions & 0 deletions docs/ai/requirements/2026-09-05-feature-builtin-remote.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
phase: requirements
title: Remote Built-in Skills Manifest Requirements
description: Define live built-in skill discovery without requiring a CLI release
---

# Remote Built-in Skills Manifest Requirements

## Problem Statement

AI DevKit compiles its curated built-in skill names into the CLI. Adding a new built-in therefore requires a CLI code change and release even though the skill itself is already installable from the AI DevKit repository.

Maintainers need a hand-managed list on `main` so a newly added skill can become a built-in immediately for existing CLI versions.

## Goals & Objectives

- Make `skills/built-in.json` on `main` the live source of truth for built-in skill names.
- Fetch the manifest whenever an init, setup, built-in install, or status flow needs the list.
- Fetch at most once per CLI process.
- Preserve working setup and built-in flows during network or manifest failures by using the current 21-name list from the latest base.
- Keep the manifest and implementation deliberately small.

### Non-goals

- Versioning or pinning the manifest or registry repository.
- Code generation or compile-time literal types derived from the manifest.
- Descriptions, compatibility metadata, or registry selection in the manifest.
- Persisting a downloaded manifest cache across CLI invocations.
- Uninstalling skills removed from the live list.

## User Stories & Use Cases

- As a maintainer, I can add `skills/<name>/SKILL.md` and append `<name>` to the JSON array on `main`, making it available to old CLIs without a release.
- As a CLI user, I receive the live curated set through `init`, `setup`, or `skill add --built-in`.
- As an offline user, setup continues with the known bundled fallback set.
- As a user running `status`, I see presence counts against the live set, or against the fallback set when the live manifest is unavailable.

## Success Criteria

- [x] `skills/built-in.json` is a bare JSON array seeded with the existing 21 names from the latest base.
- [x] A single loader fetches the raw `main` manifest and returns `Promise<readonly string[]>`.
- [x] Repeated loader calls in one process share one fetch promise.
- [x] The loader accepts only a non-empty array of non-empty, unique, valid skill-name strings.
- [x] Network, HTTP, JSON, or validation failure emits a clear warning and returns the embedded current list.
- [x] `init`, `skill add --built-in`, setup, and status obtain names through the loader.
- [x] A runtime name is passed directly to `SkillManager.addSkill`, which resolves `skills/<name>/SKILL.md` from the refreshed registry without another compiled allowlist.
- [x] Status reports `required` and `present` against the live list; on fetch failure it reports against the fallback and warns.
- [x] `BUILTIN_SKILL_NAMES` and the unused `BuiltinSkillName` literal union are removed.
- [x] Tests never use the real network and cover loader success, promise caching, invalid manifests, fallback, and consumer integration.
- [x] Build, unit tests, lint, and the E2E suite pass.

## Constraints & Assumptions

- The URL points directly to `raw.githubusercontent.com/codeaholicguy/ai-devkit/main/skills/built-in.json`.
- The repository `main` branch is intentionally the rollout boundary; old CLIs may install newer skills.
- `BUILTIN_SKILL_REGISTRY` remains trusted compiled configuration.
- The current registry refresh behavior makes newly committed skill directories visible to existing CLIs.
- The fallback may become stale; it exists only to preserve safe offline behavior.

## Questions & Open Items

None. Product and failure semantics were explicitly approved.
56 changes: 56 additions & 0 deletions docs/ai/testing/2026-09-05-feature-builtin-remote.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
phase: testing
title: Remote Built-in Skills Manifest Testing
description: Deterministic coverage for remote loading, fallback, and built-in consumers
---

# Remote Built-in Skills Manifest Testing

## Test Coverage Goals

- Cover every new loader branch and all four changed consumer boundaries.
- Mock global fetch in loader tests and mock the loader in consumer tests.
- Preserve deterministic unit and E2E execution with no live manifest requests.

## Unit Tests

### Built-in loader

- [x] Returns a valid bare-array manifest.
- [x] Reuses one in-flight/resolved promise across calls.
- [x] Falls back and warns for network failure.
- [x] Falls back and warns for non-OK HTTP responses.
- [x] Falls back and warns for invalid JSON.
- [x] Rejects non-array, empty-array, blank-name, duplicate-name, and invalid-name manifests as complete responses.

### Consumers

- [x] `skill add --built-in` installs every name from a mocked runtime list.
- [x] Setup installs every name from a mocked runtime list for the selected agent.
- [x] Init adds mocked runtime names when built-ins are selected and does not fetch when skipped.
- [x] Status passes a mocked runtime list to readiness checks and renders fixture-derived counts.
- [x] Status uses loader fallback behavior without failing the command.

## Integration and End-to-End Tests

- [x] Existing CLI command and service suites pass with deterministic mocks.
- [x] Search E2E tests for compiled skill-count assumptions and update any affected assertions.
- [x] Full E2E suite passes without accessing the live manifest.

## Verification Gates

- [x] `npm run build`
- [x] `npm test` (equivalent Nx target run serially after a shared temporary-filesystem quota failure)
- [x] `npm run lint`
- [x] `npx vitest run --config e2e/vitest.config.ts`
- [x] `npx ai-devkit@latest lint --feature builtin-remote`

## Test Data

- Small fixture lists such as `['remote-one', 'remote-two']` for consumer behavior.
- The current 21 names from the latest base only in the manifest and loader fallback.
- Mocked successful and failing `Response` objects for loader boundaries.

## Results

Focused verification passed 97 tests in seven files. The full six-project build and lint gates passed. The full unit suite passed 2177 tests across six projects when run serially to avoid the shared temporary-filesystem quota. E2E passed 41 tests. No hardcoded E2E skill-count assertions existed.
23 changes: 17 additions & 6 deletions packages/cli/src/__tests__/commands/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const {
mockIsInteractiveTerminal,
mockReconcileAndInstall,
mockGetInstallExitCode,
mockGetBuiltinSkillNames,
} = vi.hoisted(() => ({
mockConfigManager: {
exists: vi.fn(),
Expand Down Expand Up @@ -52,8 +53,11 @@ const {
mockIsInteractiveTerminal: vi.fn() as any,
mockReconcileAndInstall: vi.fn() as any,
mockGetInstallExitCode: vi.fn() as any,
mockGetBuiltinSkillNames: vi.fn() as any,
}));

const BUILTIN_SKILL_FIXTURE = ['remote-one', 'remote-two'];

vi.mock('../../services/install/install.service.js', () => ({
reconcileAndInstall: (...args: unknown[]) => mockReconcileAndInstall(...args),
getInstallExitCode: (...args: unknown[]) => mockGetInstallExitCode(...args)
Expand Down Expand Up @@ -87,6 +91,11 @@ vi.mock('../../lib/SkillManager.js', () => ({
SkillManager: vi.fn(function () { return mockSkillManager; })
}));

vi.mock('../../lib/BuiltinSkills.js', () => ({
BUILTIN_SKILL_REGISTRY: 'codeaholicguy/ai-devkit',
getBuiltinSkillNames: (...args: unknown[]) => mockGetBuiltinSkillNames(...args),
}));

vi.mock('../../lib/InitTemplate.js', () => ({
loadInitTemplate: (...args: unknown[]) => mockLoadInitTemplate(...args)
}));
Expand All @@ -100,8 +109,7 @@ vi.mock('../../util/terminal.js', () => ({
}));

import { initCommand } from '../../commands/init.js';
import { BUILTIN_SKILL_NAMES, BUILTIN_SKILL_REGISTRY } from '../../constants.js';
import { SkillManager } from '../../lib/SkillManager.js';
import { BUILTIN_SKILL_REGISTRY } from '../../lib/BuiltinSkills.js';

function confirmCallsMatching(pattern: RegExp): any[] {
return mockConfirm.mock.calls.filter(([config]: any[]) =>
Expand Down Expand Up @@ -149,6 +157,7 @@ describe('init command', () => {
warnings: [], items: [], complete: true
});
mockGetInstallExitCode.mockReturnValue(0);
mockGetBuiltinSkillNames.mockResolvedValue(BUILTIN_SKILL_FIXTURE);
});

afterEach(() => {
Expand Down Expand Up @@ -287,9 +296,9 @@ describe('init command', () => {

await initCommand({ template: './init.yaml', builtIn: true });

expect(appliedConfig().skills).toHaveLength(BUILTIN_SKILL_NAMES.length + 1);
expect(appliedConfig().skills).toHaveLength(BUILTIN_SKILL_FIXTURE.length + 1);
expect(appliedConfig().skills).toContainEqual({ registry: BUILTIN_SKILL_REGISTRY, name: 'debug' });
for (const skill of BUILTIN_SKILL_NAMES) {
for (const skill of BUILTIN_SKILL_FIXTURE) {
expect(appliedConfig().skills).toContainEqual({ registry: BUILTIN_SKILL_REGISTRY, name: skill });
}
const builtinPrompts = confirmCallsMatching(/Install AI DevKit built-in skills/);
Expand All @@ -304,8 +313,8 @@ describe('init command', () => {

await initCommand({ template: './init.yaml', builtIn: true });

expect(appliedConfig().skills).toHaveLength(BUILTIN_SKILL_NAMES.length);
for (const skill of BUILTIN_SKILL_NAMES) {
expect(appliedConfig().skills).toHaveLength(BUILTIN_SKILL_FIXTURE.length);
for (const skill of BUILTIN_SKILL_FIXTURE) {
expect(appliedConfig().skills).toContainEqual({ registry: BUILTIN_SKILL_REGISTRY, name: skill });
}
const builtinPrompts = confirmCallsMatching(/Install AI DevKit built-in skills/);
Expand Down Expand Up @@ -336,6 +345,7 @@ describe('init command', () => {
const builtinPromptCalls = confirmCallsMatching(/Install AI DevKit built-in skills/);
expect(builtinPromptCalls.length).toBe(1);
expect(mockSkillManager.addSkill).not.toHaveBeenCalled();
expect(mockGetBuiltinSkillNames).not.toHaveBeenCalled();
});

it('does not prompt for built-in skills when running in template mode', async () => {
Expand Down Expand Up @@ -376,6 +386,7 @@ describe('init command', () => {
const builtinPrompts = confirmCallsMatching(/Install AI DevKit built-in skills/);
expect(builtinPrompts).toHaveLength(0);
expect(mockSkillManager.addSkill).not.toHaveBeenCalled();
expect(mockGetBuiltinSkillNames).not.toHaveBeenCalled();
expect(mockUi.info).toHaveBeenCalledWith(
expect.stringMatching(/non-interactive|--built-in/)
);
Expand Down
Loading
Loading