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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
{
"name": "ace",
"source": "./",
"version": "0.13.278",
"version": "0.13.279",
"description": "AI Connect Engine — orchestrates the CRISPR-Connect lifecycle from idea through app building, Connect setup, LLO management, and closeout"
}
]
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ace",
"version": "0.13.278",
"version": "0.13.279",
"description": "AI Connect Engine — orchestrates the CRISPR-Connect lifecycle from idea through app building, Connect setup, LLO management, and closeout",
"author": {
"name": "Jonathan Jackson",
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ All notable changes to the ACE plugin will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and the plugin follows [semantic versioning](https://semver.org/spec/v2.0.0.html).

## 0.13.279 — 2026-05-19

**New `ocs_get_chatbot_pipeline_id` atom — closes last orphan-storage class on OCS sweep.**

Before this PR, `/ace:sweep ocs` could delete an orphan chatbot but had no way to discover its paired Pipeline row's id. The existing `PlaywrightBackend.pipelineIdFor` method (used internally by `cloneChatbot` + the pipeline-patch atoms) already scrapes the pipeline-builder HTML — but it wasn't exposed as an MCP atom, so sweep skills couldn't call it. Result: every chatbot delete left a zombie Pipeline row (`is_archived=False`, no parent chatbot in the live listing). Surfaced live in the 2026-05-18 OCS sweep that cleaned up 9 orphan chatbots + 12 collections but had to skip all 10 paired pipelines.

Fix: thin one-line wrapper exposing the existing internal method.

- `mcp/ocs/client.ts` — new `getChatbotPipelineId({ experiment_id }) → { pipeline_id }` capability.
- `mcp/ocs/backends/composite.ts` — delegates to `playwright.pipelineIdFor(experiment_id)`.
- `mcp/ocs/capability-map.ts` — new `get_chatbot_pipeline_id` entry routed to Playwright with `restTarget` noted as "pipeline_id field not yet exposed in REST schema" (lock-step swap if OCS adds it upstream).
- `mcp/ocs-server.ts` — registers the new `ocs_get_chatbot_pipeline_id` tool. Description explains the use case (sweep wiring) and the OCS-side gap (REST schema omits pipeline_id).
- `skills/sweep-ocs/SKILL.md § Process` — step 3 now calls `ocs_get_chatbot_pipeline_id` per chatbot; step 9 explicitly warns against skipping the pipeline delete.
- `test/mcp/ocs/composite.test.ts` — new case asserts the composite routes `getChatbotPipelineId` to `pipelineIdFor` and wraps the integer in `{ pipeline_id }`. 9/9 tests pass.

The 10 zombie pipelines from the 2026-05-18 sweep (chatbots 11839, 11994, 11996, 12000, 12003, 12018, 12042, 12050, 12101 + historical 12027 versions v2-v11) can be cleaned up in a follow-up sweep with the new atom in place.

## 0.13.277 — 2026-05-18

**Mirror Vellum's slug/name separation in the Nova architect brief (follow-up to 0.13.274).**
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.13.278
0.13.279
7 changes: 7 additions & 0 deletions mcp/ocs-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,13 @@ server.tool(
async (args) => result(await composite.deleteChatbot(args)),
);

server.tool(
'ocs_get_chatbot_pipeline_id',
'Resolve an experiment_id (integer chatbot id) to its working-version pipeline_id (integer). The OCS REST `/api/experiments/<id>/` response omits pipeline_id by design; this atom scrapes it from the pipeline-builder HTML (`SiteJS.pipeline.renderPipeline("#pipelineBuilder", "<team>", <pipeline_id>)`) via Playwright and caches the result per experiment_id. Used by /ace:sweep ocs to pair each orphan chatbot with its per-opp Pipeline row before deletion — without this, deleting an orphan chatbot leaves its Pipeline as a zombie row on the team (is_archived=False, no parent chatbot in the live listing). Returns `{ pipeline_id: number }`.',
{ experiment_id: z.number().int() },
async (args) => result(await composite.getChatbotPipelineId(args)),
);

server.tool(
'ocs_delete_pipeline',
'Delete a pipeline (sets is_archived=True server-side). SAFE PER-OPP: when ACE clones a chatbot, Pipeline.create_new_version(is_copy=True) deep-clones the Pipeline row + its nodes — each clone has its own pipeline. Deleting the pipeline does NOT cascade-delete its referenced Collections — those need separate ocs_delete_collection calls. Routes through Playwright to /a/<team>/pipelines/<pk>/delete/ (HTTP DELETE method on Django View.delete(); returns 200 empty body).',
Expand Down
3 changes: 3 additions & 0 deletions mcp/ocs/backends/composite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export class CompositeBackend implements OcsClient {
publishChatbotVersion = (a: Parameters<OcsClient['publishChatbotVersion']>[0]) => this.opts.playwright.publishChatbotVersion(a);
getChatbotEmbedInfo = (a: Parameters<OcsClient['getChatbotEmbedInfo']>[0]) => this.opts.playwright.getChatbotEmbedInfo(a);
deleteChatbot = (a: Parameters<OcsClient['deleteChatbot']>[0]) => this.opts.playwright.deleteChatbot(a);
getChatbotPipelineId = async (a: Parameters<OcsClient['getChatbotPipelineId']>[0]) => ({
pipeline_id: await this.opts.playwright.pipelineIdFor(a.experiment_id),
});
deletePipeline = (a: Parameters<OcsClient['deletePipeline']>[0]) => this.opts.playwright.deletePipeline(a);
deleteCollection = (a: Parameters<OcsClient['deleteCollection']>[0]) => this.opts.playwright.deleteCollection(a);

Expand Down
2 changes: 2 additions & 0 deletions mcp/ocs/capability-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type Capability =
| 'publish_chatbot_version'
| 'get_chatbot_embed_info'
| 'delete_chatbot'
| 'get_chatbot_pipeline_id'
| 'delete_pipeline'
| 'delete_collection'
// Observation (12)
Expand Down Expand Up @@ -49,6 +50,7 @@ export const CAPABILITY_MAP: Record<Capability, CapabilityRoute> = {
publish_chatbot_version: { backend: 'PLAYWRIGHT', restTarget: 'POST /api/experiments/{id}/versions/' },
get_chatbot_embed_info: { backend: 'HYBRID', restTarget: 'GET /api/experiments/{id}/embed/' },
delete_chatbot: { backend: 'PLAYWRIGHT', restTarget: 'DELETE /api/experiments/{id}/ (not yet shipped)' },
get_chatbot_pipeline_id: { backend: 'PLAYWRIGHT', restTarget: 'GET /api/experiments/{id}/ (pipeline_id field not yet exposed in REST schema)' },
delete_pipeline: { backend: 'PLAYWRIGHT', restTarget: 'DELETE /api/pipelines/{id}/ (not yet shipped)' },
delete_collection: { backend: 'PLAYWRIGHT', restTarget: 'DELETE /api/collections/{id}/ (not yet shipped)' },

Expand Down
15 changes: 15 additions & 0 deletions mcp/ocs/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,21 @@ export interface OcsClient {
*/
deleteChatbot(args: { experiment_id: number }): Promise<{ deleted: number }>;

/**
* Resolve an experiment_id (integer chatbot id) to its working-version
* pipeline_id (integer). Required by `/ace:sweep ocs` to pair each orphan
* chatbot with its per-opp Pipeline row before deletion — without this,
* deleting an orphan chatbot leaves its Pipeline as a zombie row on the
* team (`is_archived=False`, no parent chatbot in the live listing).
*
* OCS's REST `/api/experiments/<id>/` response intentionally omits
* `pipeline_id`; the value is rendered inline in the pipeline-builder
* HTML page as `SiteJS.pipeline.renderPipeline("#pipelineBuilder", ...,
* <pipeline_id>)`. This atom scrapes that page (Playwright-backed) and
* caches the result per experiment_id.
*/
getChatbotPipelineId(args: { experiment_id: number }): Promise<{ pipeline_id: number }>;

/**
* Delete a pipeline by integer id. Sets `is_archived=True` on the Pipeline
* row server-side. Each clone gets its own Pipeline (verified 2026-05-15:
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ace",
"version": "0.13.278",
"version": "0.13.279",
"description": "AI Connect Engine - orchestrator for building Connect Opps using AI",
"type": "module",
"scripts": {
Expand Down
4 changes: 2 additions & 2 deletions skills/sweep-ocs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ No dedup at the file or vector layer means uploading the same PDD into 19 per-op
3. **List OCS inventory** using existing atoms:
- `ocs_list_chatbots` (paginate as needed)
- `ocs_list_sessions` (filter to `OCS_TEAM_SLUG`; last 90 days by default).
- For each chatbot, derive its pipeline id and per-opp collection ids from the chatbot's published version description / pipeline definition. The chatbot's most-recent version description embeds the per-opp collection id explicitly (e.g. "shared Connect collection 350 + new per-run collection 418"). Parse it; cross-check by reading the pipeline's LLM node params for collection refs.
- For each chatbot, derive its pipeline_id via `ocs_get_chatbot_pipeline_id({ experiment_id })` (added 0.13.277 — scrapes the pipeline-builder HTML; cached). Per-opp collection ids come from the chatbot's most-recent published version description (e.g. "shared Connect collection 350 + new per-run collection 418"); cross-check against the pipeline's LLM node params via the pipeline-builder page if the description is thin.
4. **Diff** each item's id against the live-set:
- chatbots → `liveSet.identifiers.ocsChatbotIds` (now includes golden template).
- sessions → `liveSet.identifiers.ocsSessionIds`.
Expand All @@ -67,7 +67,7 @@ No dedup at the file or vector layer means uploading the same PDD into 19 per-op
8. **Surface to human** in chat. Prompt for approval per actionable chunk.
9. **On approval (in order, per orphan chatbot):**
- Call `ocs_delete_chatbot({ experiment_id })`.
- Call `ocs_delete_pipeline({ pipeline_id })` for the paired pipeline.
- Call `ocs_delete_pipeline({ pipeline_id })` for the paired pipeline (resolved via `ocs_get_chatbot_pipeline_id` in step 3 — never skip this; pipeline deletes are the only way to clear the per-opp Pipeline row).
- For each per-opp collection_id derived in step 3, call `ocs_delete_collection({ collection_id })` — but ONLY if collection_id ≠ `OCS_SHARED_COLLECTION_ID`. The atom itself doesn't enforce this; the skill must filter.
- For each approved orphan session, call `ocs_end_session`.

Expand Down
9 changes: 9 additions & 0 deletions test/mcp/ocs/composite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,13 @@ describe('CompositeBackend routing', () => {
await c.getChatbotEmbedInfo({ experiment_id: 1 });
expect(pw.getChatbotEmbedInfo).toHaveBeenCalled();
});

it('getChatbotPipelineId calls playwright.pipelineIdFor and wraps the result', async () => {
const rest = {};
const pw = { pipelineIdFor: vi.fn().mockResolvedValue(5942) };
const c = new CompositeBackend({ rest: rest as never, playwright: pw as never });
const out = await c.getChatbotPipelineId({ experiment_id: 12167 });
expect(pw.pipelineIdFor).toHaveBeenCalledWith(12167);
expect(out).toEqual({ pipeline_id: 5942 });
});
});
Loading