diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b403fde..e56e510 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "name": "aictrl", "source": "./claude/aictrl", "description": "Eight portable engineering skills with optional controlled AICtrl workflow execution", - "version": "0.1.0-beta.1", + "version": "0.1.0-beta.2", "author": { "name": "aictrl.dev" }, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37acbeb..564f1f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: unit: name: Unit Tests @@ -21,8 +24,65 @@ jobs: - run: npm test - run: npm run validate:public + codex-public: + name: E2E (Codex public marketplace) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install the supported Codex CLI + run: npm install --global @openai/codex@0.144.4 + - name: Run clean install, repeat, upgrade, and removal smoke + run: npm run smoke:codex-public + + claude-public: + name: E2E (Claude public marketplace) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install the supported Claude Code CLI + run: npm install --global @anthropic-ai/claude-code@2.1.210 + - name: Run clean public install, repeat, and removal smoke + env: + AICTRL_CLAUDE_MARKETPLACE_SOURCE: ${{ github.event.pull_request.head.repo.full_name || github.repository }}@${{ github.head_ref || github.ref_name }} + run: npm run smoke:claude-public + + opencode-public: + name: E2E (OpenCode public package) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install the supported OpenCode CLI + run: npm install --global opencode-ai@1.18.1 + - name: Run packed install, OAuth boundary, repeat, and removal smoke + env: + AICTRL_OPENCODE_CONNECTIVITY_ORIGIN: https://sandbox.aictrl.dev + run: npm run smoke:opencode-public + + production-mcp: + name: Smoke (production MCP catalog) + runs-on: ubuntu-latest + if: github.event_name == 'push' || !github.event.pull_request.head.repo.fork + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Verify the authenticated six-tool production catalog + env: + AICTRL_API_KEY: ${{ secrets.AICTRL_API_KEY }} + run: npm run smoke:mcp-production + e2e: - name: E2E (OpenCode against production) + name: E2E (legacy installer against production) runs-on: ubuntu-latest if: github.event_name == 'push' || !github.event.pull_request.head.repo.fork steps: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 36dfabd..b5c5e88 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,8 +18,30 @@ jobs: with: node-version: 24 registry-url: https://registry.npmjs.org - cache: npm + - name: Install npm with trusted publishing support + run: npm install --global npm@11.18.0 - run: npm ci - run: npm run build - run: npm test - - run: npm publish --access public + - run: npm run validate:public + - name: Verify production OpenCode OAuth boundary + if: startsWith(github.event.release.tag_name, 'public-v') + run: | + npm install --global opencode-ai@1.18.1 + npm run smoke:opencode-public + - name: Resolve release target + id: release + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: node scripts/resolve-release-target.mjs "$RELEASE_TAG" >> "$GITHUB_OUTPUT" + - name: Verify existing publication + id: registry + run: >- + node scripts/verify-release-publication.mjs + "${{ steps.release.outputs.package_spec }}" >> "$GITHUB_OUTPUT" + - name: Publish release target + if: steps.registry.outputs.published != 'true' + run: >- + npm publish "${{ steps.release.outputs.package_spec }}" + --access public + --tag "${{ steps.release.outputs.dist_tag }}" diff --git a/README.md b/README.md index b35a204..e5dd04a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Install eight portable engineering skills across Claude Code, Codex, and OpenCode. Every skill works locally; connected `implement-code-change` adds -versioned remote execution, approvals, evidence, history, and policy controls. +remote workflow execution, approvals, evidence, history, and policy controls. ## Public packages @@ -35,9 +35,11 @@ Use `npx @aictrl/opencode --project .` for a project-local install or ## Reproducible skill source -`public-skills.lock.json` pins an immutable `aictrl-dev/skills` commit and the -digest of its checksum manifest. All three vendor packages contain byte-identical -copies of the eight launch skills. +`public-skills.lock.json` pins an immutable +[`aictrl-dev/skills`](https://github.com/aictrl-dev/skills) release, commit, and +checksum-manifest digest. All three vendor packages contain byte-identical +copies of the eight launch skills and connect the same `aictrl` server identity +to the canonical public workflow endpoint. ```bash npm run assemble:public @@ -45,14 +47,22 @@ npm run verify:public ``` CI rejects checksum mismatches, missing/extra skills, manual generated drift, -invalid Codex metadata, and package lifecycle regressions. +invalid Codex metadata, and package lifecycle regressions. Clean-client +lifecycle jobs exercise the real Claude Code, Codex, and OpenCode CLIs against +the packed or public distribution paths, including repeated installation and +removal. The npm release job also requires OpenCode's production OAuth boundary +to reach the expected unauthenticated state before publishing. ## Release status The package tree is a public beta artifact. Connected release remains gated on -the production `https://aictrl.dev/mcp/workflows` endpoint, OAuth hardening, -clean-client lifecycle evidence, publisher verification, and vendor publication -checks. Local skills do not require an AICtrl account or API key. +the production `https://aictrl.dev/mcp` resource, OAuth hardening, clean-client +lifecycle evidence, publisher verification, and vendor publication checks. +Local skills do not require an AICtrl account or API key. + +Release owners must follow the [public release runbook](docs/public-release-runbook.md), +including the skills re-pin, first npm publication, vendor smoke tests, evidence, +and rollback gates. The existing `npx @aictrl/plugin` tenant installer remains supported and is not silently replaced by this public OAuth path. diff --git a/claude/aictrl/.claude-plugin/plugin.json b/claude/aictrl/.claude-plugin/plugin.json index 31396b2..38ebe84 100644 --- a/claude/aictrl/.claude-plugin/plugin.json +++ b/claude/aictrl/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "aictrl", - "version": "0.1.0-beta.1", + "version": "0.1.0-beta.2", "description": "Eight portable engineering skills with optional controlled AICtrl workflow execution", "author": { "name": "aictrl.dev", diff --git a/claude/aictrl/.mcp.json b/claude/aictrl/.mcp.json index bba34c4..544d452 100644 --- a/claude/aictrl/.mcp.json +++ b/claude/aictrl/.mcp.json @@ -2,7 +2,7 @@ "mcpServers": { "aictrl": { "type": "http", - "url": "https://aictrl.dev/mcp/workflows" + "url": "https://aictrl.dev/mcp" } } } diff --git a/claude/aictrl/skills/code-review/SKILL.md b/claude/aictrl/skills/code-review/SKILL.md index 5a09c15..6eaebb0 100644 --- a/claude/aictrl/skills/code-review/SKILL.md +++ b/claude/aictrl/skills/code-review/SKILL.md @@ -46,4 +46,4 @@ Produce high-signal findings for the exact pull or merge request revision. Revie - Separate verified defects from residual risk and untested hypotheses. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=code-review) diff --git a/claude/aictrl/skills/create-bug/SKILL.md b/claude/aictrl/skills/create-bug/SKILL.md index 17130bf..1c714be 100644 --- a/claude/aictrl/skills/create-bug/SKILL.md +++ b/claude/aictrl/skills/create-bug/SKILL.md @@ -61,4 +61,4 @@ Turn symptoms, logs, or an observed regression into a bug that another engineer - Do not implement the fix, commit, push, or change issue state unless the user asks. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug&utm_listing=github-skills&utm_platform=portable&utm_skill=create-bug).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug&utm_listing=github-skills&utm_platform=portable&utm_skill=create-bug) diff --git a/claude/aictrl/skills/create-issue/SKILL.md b/claude/aictrl/skills/create-issue/SKILL.md index 8e50dde..b26fa91 100644 --- a/claude/aictrl/skills/create-issue/SKILL.md +++ b/claude/aictrl/skills/create-issue/SKILL.md @@ -64,4 +64,4 @@ so that . - Never include secrets, credentials, private customer data, or unnecessary source excerpts. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue&utm_listing=github-skills&utm_platform=portable&utm_skill=create-issue).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue&utm_listing=github-skills&utm_platform=portable&utm_skill=create-issue) diff --git a/claude/aictrl/skills/create-workflow/SKILL.md b/claude/aictrl/skills/create-workflow/SKILL.md index 9eae3f0..00d56dd 100644 --- a/claude/aictrl/skills/create-workflow/SKILL.md +++ b/claude/aictrl/skills/create-workflow/SKILL.md @@ -5,7 +5,7 @@ description: Create and validate an AICtrl workflow v2 YAML file with typed para # Create an AICtrl Workflow -Author a reviewable `.aictrl/workflows/.yaml` file. Workflow v2 with inline `task` nodes is the default because it keeps task configuration portable in Git. +Author a reviewable `.aictrl/workflows/.yaml` file. Treat this directory like `.github/workflows/`: create the workflow in the repository whose automation the user is defining. Installing a skill or plugin does not publish a workflow for that repository. Workflow v2 with inline `task` nodes is the default because it keeps task configuration portable in Git. ## Workflow @@ -37,17 +37,18 @@ Author a reviewable `.aictrl/workflows/.yaml` file. Workflow v2 with schemaVersion: aictrl/workflow/v2 name: implement-change parameters: - - { name: repository, type: string, required: true } - - { name: issue-id, type: number, required: true } + - { name: repository, type: repository, required: true } + - { name: issue-id, type: number, required: true, validation: { min: 1 } } nodes: - id: implement type: task skill: implement-code-change@1.0.0 taskType: general prompt: Implement the requested issue and produce a merge-ready pull request. + timeoutMinutes: 10 parameters: - - { name: repository, type: string, required: true } - - { name: issue-id, type: number, required: true } + - { name: repository, type: repository, required: true } + - { name: issue-id, type: number, required: true, validation: { min: 1 } } inputs: repository: { from: input, name: repository } issue-id: { from: input, name: issue-id } @@ -64,4 +65,4 @@ nodes: - Canvas positions and runtime fields are platform-owned and must not be authored. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow&utm_listing=github-skills&utm_platform=portable&utm_skill=create-workflow).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow&utm_listing=github-skills&utm_platform=portable&utm_skill=create-workflow) diff --git a/claude/aictrl/skills/create-workflow/reference/authoring-guide.md b/claude/aictrl/skills/create-workflow/reference/authoring-guide.md index 3497c04..f9bffc1 100644 --- a/claude/aictrl/skills/create-workflow/reference/authoring-guide.md +++ b/claude/aictrl/skills/create-workflow/reference/authoring-guide.md @@ -39,7 +39,7 @@ triggers: [...] # optional; up to 10 file-declared event tri the platform on import. Authoring them is a schema error (`additionalProperties` is `false` at the top level and on every object). -## Parameter types (12 total) +## Parameter types (13 total) Used for workflow-level `parameters`, `user-input` node `parameters`, and inline `task` node `parameters`: @@ -54,6 +54,7 @@ Used for workflow-level `parameters`, `user-input` node `parameters`, and inline | `select` | string | Requires `options: [...]` | | `multi-select` | list | Requires `options: [...]` | | `json` | dyn | Arbitrary JSON | +| `repository` | string | Connected repository in owner/name form | | `pull-request` | string | PR URL | | `story` | string | Story/ticket URL | | `image` | string | Image URL or base64 | @@ -88,6 +89,7 @@ layer-1 error. skill: code-review@1.0.0 # required; pin when a version is resolvable taskType: code-review # required; executor tool boundary prompt: Review the current pull request head and return structured findings. + timeoutMinutes: 10 # optional; executor runtime, integer 1-60 parameters: - { name: pull-request, type: pull-request, required: true } inputs: @@ -101,6 +103,7 @@ Inline task nodes make the task configuration portable in the workflow file. Their `inputs` are checked against the in-file `parameters`; their declared `outputs` may be mapped by downstream nodes. Treat `prompt` as instructions for the selected skill, not as a way to relax workflow policy or tool boundaries. +`timeoutMinutes` is an optional hard executor bound from 1 to 60 minutes. ### `template` — run a task template ```yaml diff --git a/claude/aictrl/skills/create-workflow/reference/v1/workflow.schema.json b/claude/aictrl/skills/create-workflow/reference/v1/workflow.schema.json index 442541b..be88a9a 100644 --- a/claude/aictrl/skills/create-workflow/reference/v1/workflow.schema.json +++ b/claude/aictrl/skills/create-workflow/reference/v1/workflow.schema.json @@ -34,7 +34,7 @@ "name": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" }, "label": { "type": "string" }, "type": { - "enum": ["string", "text", "number", "boolean", "url", "select", "multi-select", "json", "pull-request", "story", "image", "github-issue"] + "enum": ["string", "text", "number", "boolean", "url", "select", "multi-select", "json", "repository", "pull-request", "story", "image", "github-issue"] }, "description": { "type": "string" }, "required": { "type": "boolean", "default": false }, diff --git a/claude/aictrl/skills/create-workflow/reference/workflow.schema.json b/claude/aictrl/skills/create-workflow/reference/workflow.schema.json index 0902b84..16fe7f0 100644 --- a/claude/aictrl/skills/create-workflow/reference/workflow.schema.json +++ b/claude/aictrl/skills/create-workflow/reference/workflow.schema.json @@ -87,8 +87,8 @@ "then": { "required": ["parameters"], "not": { "anyOf": [ { "required": ["template"] }, { "required": ["templateVersion"] }, { "required": ["workflow"] }, { "required": ["workflowVersion"] }, { "required": ["maxIterations"] }, { "required": ["until"] }, { "required": ["while"] }, { "required": ["onMaxIterations"] }, { "required": ["body"] }, { "required": ["signalSource"] }, { "required": ["timeoutMinutes"] }, { "required": ["checklistItems"] }, { "required": ["assignee"] }, { "required": ["skill"] }, { "required": ["taskType"] }, { "required": ["prompt"] }, { "required": ["outputs"] } ] } } }, { "if": { "properties": { "type": { "const": "task" } }, "required": ["type"] }, - "then": { "required": ["skill"], "not": { "anyOf": [ - { "required": ["template"] }, { "required": ["templateVersion"] }, { "required": ["workflow"] }, { "required": ["workflowVersion"] }, { "required": ["maxIterations"] }, { "required": ["until"] }, { "required": ["while"] }, { "required": ["onMaxIterations"] }, { "required": ["body"] }, { "required": ["signalSource"] }, { "required": ["timeoutMinutes"] }, { "required": ["checklistItems"] }, { "required": ["assignee"] } ] } } } + "then": { "required": ["skill"], "properties": { "timeoutMinutes": { "maximum": 60 } }, "not": { "anyOf": [ + { "required": ["template"] }, { "required": ["templateVersion"] }, { "required": ["workflow"] }, { "required": ["workflowVersion"] }, { "required": ["maxIterations"] }, { "required": ["until"] }, { "required": ["while"] }, { "required": ["onMaxIterations"] }, { "required": ["body"] }, { "required": ["signalSource"] }, { "required": ["checklistItems"] }, { "required": ["assignee"] } ] } } } ] }, "inputMapping": { "$ref": "../v1/workflow.schema.json#/$defs/inputMapping" }, diff --git a/claude/aictrl/skills/implement-code-change/SKILL.md b/claude/aictrl/skills/implement-code-change/SKILL.md index 4c18632..bd8e270 100644 --- a/claude/aictrl/skills/implement-code-change/SKILL.md +++ b/claude/aictrl/skills/implement-code-change/SKILL.md @@ -28,12 +28,14 @@ Take one engineering issue to a verified, merge-ready pull request. Never merge ## Connected workflow -1. Call `list_workflows` and confirm `implement-code-change` is available. -2. Call `get_workflow` and show the resolved version, required `{ repository, issue_id }` inputs, side effects, limits, and approval gates. +Connected workflows are repository-owned configuration, analogous to GitHub Actions. Installing this skill or its plugin does not provision one. If the organization has no suitable published workflow, explain that the user can invoke `create-workflow` in the repository they want to automate; do not create or publish a workflow without a separate request. + +1. Call `list_workflows` and confirm a suitable `implement-code-change` workflow is available. If it is absent, stop the connected path and offer the repository-owned `create-workflow` path. +2. Call `get_workflow` and show the resolved immutable version, required `{ repository, issue-id }` inputs, side effects, limits, and approval gates. Preserve the hyphenated `issue-id` key exactly in tool input. 3. Obtain explicit confirmation to start if the user has not already authorized connected execution. 4. Call `start_workflow` with the resolved workflow/version, validated inputs, and a stable idempotency key derived from repository and issue. 5. Poll with `get_workflow_run`; report actual status and required action without inventing progress. -6. For a paused gate, show the revision, evidence, cost, and requested decision. Use `approve_workflow_step` only for the user's explicit approve/reject choice. +6. For a paused gate, show the revision, evidence, cost, and requested decision. Use `approve_workflow_step` only for the user's explicit approve/reject choice, passing `decision` and the unchanged 40-character `expected_revision` returned by `get_workflow_run`. If the revision changed or is absent, stop and retrieve the run again instead of deciding the gate. 7. Use `cancel_workflow_run` when explicitly requested or when the documented safety boundary requires termination. 8. Finish with the exact revision, PR/result link, checks, evidence, cost, and any actionable terminal failure. @@ -46,4 +48,4 @@ Take one engineering issue to a verified, merge-ready pull request. Never merge - The result is merge-ready, not automatically merged or deployed. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change&utm_listing=github-skills&utm_platform=portable&utm_skill=implement-code-change).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change&utm_listing=github-skills&utm_platform=portable&utm_skill=implement-code-change) diff --git a/claude/aictrl/skills/judge-review-findings/SKILL.md b/claude/aictrl/skills/judge-review-findings/SKILL.md index d5ad8ae..9f30f17 100644 --- a/claude/aictrl/skills/judge-review-findings/SKILL.md +++ b/claude/aictrl/skills/judge-review-findings/SKILL.md @@ -41,4 +41,4 @@ For every `DEFER`, name the follow-up issue or return a complete follow-up draft - Do not apply a judgment to a different head revision. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings&utm_listing=github-skills&utm_platform=portable&utm_skill=judge-review-findings).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings&utm_listing=github-skills&utm_platform=portable&utm_skill=judge-review-findings) diff --git a/claude/aictrl/skills/reply-to-code-review/SKILL.md b/claude/aictrl/skills/reply-to-code-review/SKILL.md index a819360..8d2f8d0 100644 --- a/claude/aictrl/skills/reply-to-code-review/SKILL.md +++ b/claude/aictrl/skills/reply-to-code-review/SKILL.md @@ -42,4 +42,4 @@ Turn review feedback into verified remediation and concise, evidence-backed resp - Do not create endless automated reviewer loops. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=reply-to-code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=reply-to-code-review) diff --git a/claude/aictrl/skills/spec-review/SKILL.md b/claude/aictrl/skills/spec-review/SKILL.md index 09564e4..e809bfa 100644 --- a/claude/aictrl/skills/spec-review/SKILL.md +++ b/claude/aictrl/skills/spec-review/SKILL.md @@ -56,4 +56,4 @@ Decide whether an issue is ready to implement by comparing its claims and accept - If the issue targets a different revision or repository, stop and resolve the mismatch. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review&utm_listing=github-skills&utm_platform=portable&utm_skill=spec-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review&utm_listing=github-skills&utm_platform=portable&utm_skill=spec-review) diff --git a/docs/public-release-runbook.md b/docs/public-release-runbook.md new file mode 100644 index 0000000..43e1245 --- /dev/null +++ b/docs/public-release-runbook.md @@ -0,0 +1,231 @@ +# Public agent package release runbook + +Use this runbook for the AICtrl public packages distributed through Claude Code, +Codex/ChatGPT, and OpenCode. A release is complete only when the public artifact +installs from a clean client and the connected workflow reaches a verified +terminal result. Creating a tag, submitting a listing, or passing package CI is +not publication by itself. + +## Owners and access + +Before starting, record one primary and one backup owner for each item in the +release evidence: + +- AICtrl runtime deployment and rollback; +- `aictrl-dev/skills` release and checksums; +- `aictrl-dev/aictrl-plugin` release and GitHub environments; +- the `@aictrl` npm organization and package; +- the OpenAI publisher identity, plugin portal, domain, reviewer account, and + dedicated public reviewer-fixture repository; +- the OpenCode Ecosystem contribution; +- incident response and customer communication. + +Confirm that the owners can use the production accounts before creating a +release. Never put a token, recovery code, reviewer password, or challenge value +in this repository or in release evidence. + +## Stop conditions + +Stop the release when any of these is true: + +- the pinned skills commit or checksum manifest does not match generated files; +- the public MCP catalog is not exactly the six documented workflow lifecycle + tools; +- a tool schema or annotation differs from deployed behavior; +- OAuth, tenant, repository, workflow, run, or revision authorization fails a + negative test; +- production health or the OpenCode OAuth-boundary smoke test fails; +- package, secret, lifecycle, or clean-install validation fails; +- support, privacy, terms, publisher identity, reviewer access, reproducible + fixture data, or rollback ownership is incomplete. + +## Promotion order + +1. Merge the runtime and domain-verification changes to `sandbox` and wait for a + healthy deployment. +2. Apply the canonical `implement-code-change` workflow in sandbox. Verify its + immutable version, `repository` and `issue-id` inputs, exact-revision gate, + two task bounds, and no-merge/no-deploy boundary. +3. Publish the canonical skills commit as a semantic release and verify the + published `CHECKSUMS.sha256` against that exact release commit. For this beta, + use `aictrl-dev/skills@v1.0.1` at commit + `01cf2fa9b9bcf3b3b48cb00f3e48764c292ecf18`. +4. Pin that exact commit and checksum-manifest digest in + `public-skills.lock.json`. Run `npm run assemble:public`; do not hand-edit + generated skill files. +5. Run all package verification below, but do not merge or publish the plugin + yet. +6. Promote the sandbox runtime batch to production and repeat the health, OAuth, + MCP catalog, schema, annotation, and connected-workflow checks against the + canonical `https://aictrl.dev/mcp` resource. Stop if the authenticated + `tools/list` result is not exactly the six documented lifecycle tools. The + plugin CI performs the catalog, schema, and annotation scan with the + protected `AICTRL_API_KEY`; release owners can repeat it without printing the + key by running `npm run smoke:mcp-production` with that variable exported. +7. Merge the plugin PR only after step 6 passes, then publish and verify the + public Git, npm, and portal artifacts in the vendor sections below. + +Do not reorder steps 3 and 4 or steps 6 and 7. The package must contain the +hyphenated `issue-id` workflow input, and its canonical MCP URL must be deployed +and verified before the public plugin is merged. + +## Package verification + +Run from a clean checkout of the exact plugin release commit: + +```bash +npm ci +npm run build +npm test +npm run validate:public +npm run smoke:claude-public +npm run smoke:codex-public +npm run smoke:opencode-public +npm pack ./opencode --dry-run --json +``` + +Record the commit, skill source commit, checksum-manifest digest, package +versions, command results, client versions, and UTC time. The packed OpenCode +artifact must contain only the installer, README, license, skills manifest, and +the eight generated skills. + +## Claude Code publication + +1. Publish the final plugin commit and release notes in the public Git + repository. +2. From a clean Claude Code client, add `aictrl-dev/aictrl-plugin`, install + `aictrl@aictrl-public`, and start a new session. +3. Complete one local skill without authentication. +4. Complete native OAuth and the connected `implement-code-change` test through + result retrieval. +5. Repeat install, upgrade, uninstall, and marketplace removal while confirming + unrelated client state is preserved. + +Claude publication has no separate third-party directory gate unless Anthropic +publishes one. The public Git marketplace and clean external smoke test are the +release evidence. + +## OpenCode npm publication + +### First package publication + +`@aictrl/opencode` must exist before npm trusted publishing can be configured. +For the first beta only, an authenticated `@aictrl` npm owner publishes the +verified package from the merged release commit: + +```bash +npm install --global npm@11.18.0 +npm --version +npm whoami +npm publish ./opencode --access public --tag beta +``` + +Use the same pinned npm version as the release workflow so the package integrity +is reproducible. The owner must satisfy npm's current 2FA policy. Do not add a +long-lived npm token to the repository. After the package exists, configure its +trusted publisher for GitHub organization `aictrl-dev`, repository +`aictrl-plugin`, workflow `publish.yml`, environment `release`, and the +`npm publish` action. +Then publish the matching `public-v` GitHub +release from the same commit. The workflow verifies that the manually published +package has the exact local package integrity and skips the duplicate publish; +an existing version with different contents fails the release. + +### Later beta publications + +Create a GitHub release whose tag exactly matches +`public-v`. The release workflow validates the +package and production OAuth boundary, then publishes `./opencode` with the +`beta` dist-tag through npm OIDC. + +For every npm publication, verify from an unauthenticated clean environment: + +```bash +npm view @aictrl/opencode version dist-tags --json +npx @aictrl/opencode --project . +opencode mcp list +npx @aictrl/opencode --project . --uninstall +``` + +Only after the package is publicly installable, submit the one-row AICtrl +OpenCode Ecosystem change using `submission/opencode/ecosystem.md`. The current +upstream policy requires an issue before the PR. Keep its issue, PR, merge +commit, and listing smoke test in the release evidence. + +## Codex and ChatGPT publication + +1. Confirm the OpenAI organization has a verified developer or business + identity and the submitter has Apps Management write access. +2. Enter the canonical Codex MCP resource URL from `plugins/aictrl/.mcp.json` + in the plugin portal: `https://aictrl.dev/mcp`. +3. After the portal issues the domain token, temporarily set + `OPENAI_APPS_CHALLENGE_TOKEN` directly on the production Cloud Run service + using the backend OAuth runbook. Verify that + `/.well-known/openai-apps-challenge` returns only the exact token as plain + text. Keep the override only while OpenAI may need to verify it, reapply it + after any intervening Terraform deployment, and remove it from Cloud Run + after review. Never add the token to Terraform, Secret Manager, CI, shell + history, or release evidence. +4. Scan tools and confirm exactly six tools with truthful schemas, + `readOnlyHint`, `openWorldHint`, and `destructiveHint` annotations. +5. Upload the final generated skill tree, listing assets, starter prompts, + release notes, and exactly five positive plus three negative reviewer cases. + Every positive case must name its real fixture and account requirements; do + not submit placeholder repository, issue, pull-request, or run identifiers. +6. Verify the reviewer account works without MFA, email confirmation, SMS, or + private-network access. Select only supported regions and complete policy + attestations after the final review. +7. Submit for review, answer findings against the deployed version, and record + approval. Publication still requires the release owner to select Publish in + the portal after approval. +8. Install from the universal plugin directory in a clean Codex/ChatGPT client; + complete one local skill and one connected workflow before closing the vendor + issue. + +## Connected beta evidence + +The launch proof must use a dedicated, authorized public fixture repository, a +disposable fixture issue, and the no-MFA reviewer demo account. Do not run the +submission cases against a production backlog issue. Provision and rehearse the +resources using `submission/codex/reviewer-fixture.md`. Record: + +- referral source, agent platform, skill and plugin versions, without source or + prompt content; +- OAuth start, completion, and negative/cancel paths; +- the resolved workflow version and idempotent start; +- a pause at the exact base revision and an approval using the unchanged + expected revision; +- terminal status, pull-request URL, exact head revision, redacted evidence, + checks, reported cost, and errors; +- elapsed time, with the beta median under five minutes. + +The workflow must stop at a merge-ready pull request. Never use launch evidence +that merged or deployed automatically. + +## Rollback and credential rotation + +If a public artifact or connected path is unsafe: + +1. Stop new connected starts at the runtime or policy boundary while preserving + run and approval audit history. +2. Roll back the runtime to the last verified revision and repeat health and + authorization smoke tests. +3. Publish a corrected Git package version. Do not rewrite an immutable skills + tag or checksum manifest. +4. Deprecate an affected npm version and move the `beta` dist-tag to the last + safe version. Use npm unpublish only when its current policy permits it and + the incident owner explicitly approves the irreversible action. +5. Unpublish or disable the Codex plugin in the portal when the listing itself + is unsafe; notify reviewers when an in-review version is withdrawn. +6. Rotate any affected OAuth client, npm publisher, GitHub release, reviewer, + domain-challenge, or runtime secret. Revoke the old credential before + restoring publication. +7. Publish an incident note through the support/status channel and link the + replacement release. + +## Post-publication monitoring + +For the first 24 hours, the release owner monitors production health, OAuth +completion/failure, workflow starts and terminal states, authorization denials, +latency, cost, and support reports. Re-run the public install and connected smoke +test after any runtime, skill, package, listing, or credential change. diff --git a/opencode/LICENSE b/opencode/LICENSE new file mode 100644 index 0000000..68ff8cb --- /dev/null +++ b/opencode/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 aictrl.dev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/opencode/README.md b/opencode/README.md index e211422..a6b59c7 100644 --- a/opencode/README.md +++ b/opencode/README.md @@ -11,6 +11,13 @@ opencode mcp auth aictrl The skills work locally without an AICtrl account. OAuth is requested only when you use the connected `implement-code-change` workflow. +The package is assembled from the pinned, checksummed +[`aictrl-dev/skills`](https://github.com/aictrl-dev/skills) release; the canonical +skills remain free to install directly on any supported coding agent. + Use `npx @aictrl/opencode --project .` for project-local installation or `npx @aictrl/opencode --uninstall` to remove only AICtrl-managed skills and MCP configuration. + +Support: https://aictrl.dev/support · Privacy: https://aictrl.dev/privacy · +Terms: https://aictrl.dev/terms diff --git a/opencode/bin/install.js b/opencode/bin/install.js index 89e3cc3..03c6c6e 100755 --- a/opencode/bin/install.js +++ b/opencode/bin/install.js @@ -16,7 +16,10 @@ const configRoot = projectRoot const skillsRoot = join(configRoot, 'skills'); const configFile = projectRoot ? resolve(projectRoot, 'opencode.json') : join(configRoot, 'opencode.json'); const sourceSkills = join(packageRoot, 'skills'); -const skillNames = readSkillNames(sourceSkills); +const skillsManifest = readSkillsManifest(sourceSkills); +readPackageMetadata(); +const skillNames = [...new Set(skillsManifest.skills)].sort(); +const mcpUrl = 'https://aictrl.dev/mcp'; if (args.has('--uninstall')) { for (const skill of skillNames) rmSync(join(skillsRoot, skill), { recursive: true, force: true }); @@ -43,7 +46,7 @@ const mcp = config.mcp && typeof config.mcp === 'object' && !Array.isArray(confi : {}; mcp.aictrl = { type: 'remote', - url: 'https://aictrl.dev/mcp/workflows', + url: mcpUrl, enabled: true, }; config.$schema ||= 'https://opencode.ai/config.json'; @@ -53,13 +56,30 @@ writeJson(configFile, config); console.log(`Installed ${skillNames.length} AICtrl skills and OAuth MCP config.`); console.log('Start a new OpenCode session, then run: opencode mcp auth aictrl'); -function readSkillNames(directory) { +function readSkillsManifest(directory) { if (!existsSync(directory)) fail('Bundled skills are missing; reinstall the package.'); const manifest = JSON.parse(readFileSync(new URL('../skills-manifest.json', import.meta.url), 'utf8')); - if (!Array.isArray(manifest) || manifest.some((name) => typeof name !== 'string')) { + if ( + !manifest + || typeof manifest !== 'object' + || !Array.isArray(manifest.skills) + || manifest.skills.some((name) => typeof name !== 'string') + || !isVersion(manifest.skillsVersion) + ) { fail('Bundled skills manifest is invalid; reinstall the package.'); } - return [...new Set(manifest)].sort(); + return manifest; +} + +function readPackageMetadata() { + const metadata = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')); + if (metadata.name !== '@aictrl/opencode' || !isVersion(metadata.version)) { + fail('Package metadata is invalid; reinstall the package.'); + } +} + +function isVersion(value) { + return typeof value === 'string' && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(value); } function readConfig(file) { diff --git a/opencode/package.json b/opencode/package.json index dc32d65..acbd24d 100644 --- a/opencode/package.json +++ b/opencode/package.json @@ -1,6 +1,6 @@ { "name": "@aictrl/opencode", - "version": "0.1.0-beta.1", + "version": "0.1.0-beta.2", "description": "Install AICtrl engineering skills and OAuth workflow MCP for OpenCode", "type": "module", "bin": { @@ -26,5 +26,9 @@ }, "engines": { "node": ">=18.0.0" + }, + "publishConfig": { + "access": "public", + "tag": "beta" } } diff --git a/opencode/skills-manifest.json b/opencode/skills-manifest.json index 32e1c26..2a334c1 100644 --- a/opencode/skills-manifest.json +++ b/opencode/skills-manifest.json @@ -1,10 +1,13 @@ -[ - "create-issue", - "create-bug", - "spec-review", - "implement-code-change", - "code-review", - "judge-review-findings", - "reply-to-code-review", - "create-workflow" -] +{ + "skillsVersion": "1.0.1", + "skills": [ + "create-issue", + "create-bug", + "spec-review", + "implement-code-change", + "code-review", + "judge-review-findings", + "reply-to-code-review", + "create-workflow" + ] +} diff --git a/opencode/skills/code-review/SKILL.md b/opencode/skills/code-review/SKILL.md index 5a09c15..6eaebb0 100644 --- a/opencode/skills/code-review/SKILL.md +++ b/opencode/skills/code-review/SKILL.md @@ -46,4 +46,4 @@ Produce high-signal findings for the exact pull or merge request revision. Revie - Separate verified defects from residual risk and untested hypotheses. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=code-review) diff --git a/opencode/skills/create-bug/SKILL.md b/opencode/skills/create-bug/SKILL.md index 17130bf..1c714be 100644 --- a/opencode/skills/create-bug/SKILL.md +++ b/opencode/skills/create-bug/SKILL.md @@ -61,4 +61,4 @@ Turn symptoms, logs, or an observed regression into a bug that another engineer - Do not implement the fix, commit, push, or change issue state unless the user asks. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug&utm_listing=github-skills&utm_platform=portable&utm_skill=create-bug).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug&utm_listing=github-skills&utm_platform=portable&utm_skill=create-bug) diff --git a/opencode/skills/create-issue/SKILL.md b/opencode/skills/create-issue/SKILL.md index 8e50dde..b26fa91 100644 --- a/opencode/skills/create-issue/SKILL.md +++ b/opencode/skills/create-issue/SKILL.md @@ -64,4 +64,4 @@ so that . - Never include secrets, credentials, private customer data, or unnecessary source excerpts. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue&utm_listing=github-skills&utm_platform=portable&utm_skill=create-issue).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue&utm_listing=github-skills&utm_platform=portable&utm_skill=create-issue) diff --git a/opencode/skills/create-workflow/SKILL.md b/opencode/skills/create-workflow/SKILL.md index 9eae3f0..00d56dd 100644 --- a/opencode/skills/create-workflow/SKILL.md +++ b/opencode/skills/create-workflow/SKILL.md @@ -5,7 +5,7 @@ description: Create and validate an AICtrl workflow v2 YAML file with typed para # Create an AICtrl Workflow -Author a reviewable `.aictrl/workflows/.yaml` file. Workflow v2 with inline `task` nodes is the default because it keeps task configuration portable in Git. +Author a reviewable `.aictrl/workflows/.yaml` file. Treat this directory like `.github/workflows/`: create the workflow in the repository whose automation the user is defining. Installing a skill or plugin does not publish a workflow for that repository. Workflow v2 with inline `task` nodes is the default because it keeps task configuration portable in Git. ## Workflow @@ -37,17 +37,18 @@ Author a reviewable `.aictrl/workflows/.yaml` file. Workflow v2 with schemaVersion: aictrl/workflow/v2 name: implement-change parameters: - - { name: repository, type: string, required: true } - - { name: issue-id, type: number, required: true } + - { name: repository, type: repository, required: true } + - { name: issue-id, type: number, required: true, validation: { min: 1 } } nodes: - id: implement type: task skill: implement-code-change@1.0.0 taskType: general prompt: Implement the requested issue and produce a merge-ready pull request. + timeoutMinutes: 10 parameters: - - { name: repository, type: string, required: true } - - { name: issue-id, type: number, required: true } + - { name: repository, type: repository, required: true } + - { name: issue-id, type: number, required: true, validation: { min: 1 } } inputs: repository: { from: input, name: repository } issue-id: { from: input, name: issue-id } @@ -64,4 +65,4 @@ nodes: - Canvas positions and runtime fields are platform-owned and must not be authored. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow&utm_listing=github-skills&utm_platform=portable&utm_skill=create-workflow).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow&utm_listing=github-skills&utm_platform=portable&utm_skill=create-workflow) diff --git a/opencode/skills/create-workflow/reference/authoring-guide.md b/opencode/skills/create-workflow/reference/authoring-guide.md index 3497c04..f9bffc1 100644 --- a/opencode/skills/create-workflow/reference/authoring-guide.md +++ b/opencode/skills/create-workflow/reference/authoring-guide.md @@ -39,7 +39,7 @@ triggers: [...] # optional; up to 10 file-declared event tri the platform on import. Authoring them is a schema error (`additionalProperties` is `false` at the top level and on every object). -## Parameter types (12 total) +## Parameter types (13 total) Used for workflow-level `parameters`, `user-input` node `parameters`, and inline `task` node `parameters`: @@ -54,6 +54,7 @@ Used for workflow-level `parameters`, `user-input` node `parameters`, and inline | `select` | string | Requires `options: [...]` | | `multi-select` | list | Requires `options: [...]` | | `json` | dyn | Arbitrary JSON | +| `repository` | string | Connected repository in owner/name form | | `pull-request` | string | PR URL | | `story` | string | Story/ticket URL | | `image` | string | Image URL or base64 | @@ -88,6 +89,7 @@ layer-1 error. skill: code-review@1.0.0 # required; pin when a version is resolvable taskType: code-review # required; executor tool boundary prompt: Review the current pull request head and return structured findings. + timeoutMinutes: 10 # optional; executor runtime, integer 1-60 parameters: - { name: pull-request, type: pull-request, required: true } inputs: @@ -101,6 +103,7 @@ Inline task nodes make the task configuration portable in the workflow file. Their `inputs` are checked against the in-file `parameters`; their declared `outputs` may be mapped by downstream nodes. Treat `prompt` as instructions for the selected skill, not as a way to relax workflow policy or tool boundaries. +`timeoutMinutes` is an optional hard executor bound from 1 to 60 minutes. ### `template` — run a task template ```yaml diff --git a/opencode/skills/create-workflow/reference/v1/workflow.schema.json b/opencode/skills/create-workflow/reference/v1/workflow.schema.json index 442541b..be88a9a 100644 --- a/opencode/skills/create-workflow/reference/v1/workflow.schema.json +++ b/opencode/skills/create-workflow/reference/v1/workflow.schema.json @@ -34,7 +34,7 @@ "name": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" }, "label": { "type": "string" }, "type": { - "enum": ["string", "text", "number", "boolean", "url", "select", "multi-select", "json", "pull-request", "story", "image", "github-issue"] + "enum": ["string", "text", "number", "boolean", "url", "select", "multi-select", "json", "repository", "pull-request", "story", "image", "github-issue"] }, "description": { "type": "string" }, "required": { "type": "boolean", "default": false }, diff --git a/opencode/skills/create-workflow/reference/workflow.schema.json b/opencode/skills/create-workflow/reference/workflow.schema.json index 0902b84..16fe7f0 100644 --- a/opencode/skills/create-workflow/reference/workflow.schema.json +++ b/opencode/skills/create-workflow/reference/workflow.schema.json @@ -87,8 +87,8 @@ "then": { "required": ["parameters"], "not": { "anyOf": [ { "required": ["template"] }, { "required": ["templateVersion"] }, { "required": ["workflow"] }, { "required": ["workflowVersion"] }, { "required": ["maxIterations"] }, { "required": ["until"] }, { "required": ["while"] }, { "required": ["onMaxIterations"] }, { "required": ["body"] }, { "required": ["signalSource"] }, { "required": ["timeoutMinutes"] }, { "required": ["checklistItems"] }, { "required": ["assignee"] }, { "required": ["skill"] }, { "required": ["taskType"] }, { "required": ["prompt"] }, { "required": ["outputs"] } ] } } }, { "if": { "properties": { "type": { "const": "task" } }, "required": ["type"] }, - "then": { "required": ["skill"], "not": { "anyOf": [ - { "required": ["template"] }, { "required": ["templateVersion"] }, { "required": ["workflow"] }, { "required": ["workflowVersion"] }, { "required": ["maxIterations"] }, { "required": ["until"] }, { "required": ["while"] }, { "required": ["onMaxIterations"] }, { "required": ["body"] }, { "required": ["signalSource"] }, { "required": ["timeoutMinutes"] }, { "required": ["checklistItems"] }, { "required": ["assignee"] } ] } } } + "then": { "required": ["skill"], "properties": { "timeoutMinutes": { "maximum": 60 } }, "not": { "anyOf": [ + { "required": ["template"] }, { "required": ["templateVersion"] }, { "required": ["workflow"] }, { "required": ["workflowVersion"] }, { "required": ["maxIterations"] }, { "required": ["until"] }, { "required": ["while"] }, { "required": ["onMaxIterations"] }, { "required": ["body"] }, { "required": ["signalSource"] }, { "required": ["checklistItems"] }, { "required": ["assignee"] } ] } } } ] }, "inputMapping": { "$ref": "../v1/workflow.schema.json#/$defs/inputMapping" }, diff --git a/opencode/skills/implement-code-change/SKILL.md b/opencode/skills/implement-code-change/SKILL.md index 4c18632..bd8e270 100644 --- a/opencode/skills/implement-code-change/SKILL.md +++ b/opencode/skills/implement-code-change/SKILL.md @@ -28,12 +28,14 @@ Take one engineering issue to a verified, merge-ready pull request. Never merge ## Connected workflow -1. Call `list_workflows` and confirm `implement-code-change` is available. -2. Call `get_workflow` and show the resolved version, required `{ repository, issue_id }` inputs, side effects, limits, and approval gates. +Connected workflows are repository-owned configuration, analogous to GitHub Actions. Installing this skill or its plugin does not provision one. If the organization has no suitable published workflow, explain that the user can invoke `create-workflow` in the repository they want to automate; do not create or publish a workflow without a separate request. + +1. Call `list_workflows` and confirm a suitable `implement-code-change` workflow is available. If it is absent, stop the connected path and offer the repository-owned `create-workflow` path. +2. Call `get_workflow` and show the resolved immutable version, required `{ repository, issue-id }` inputs, side effects, limits, and approval gates. Preserve the hyphenated `issue-id` key exactly in tool input. 3. Obtain explicit confirmation to start if the user has not already authorized connected execution. 4. Call `start_workflow` with the resolved workflow/version, validated inputs, and a stable idempotency key derived from repository and issue. 5. Poll with `get_workflow_run`; report actual status and required action without inventing progress. -6. For a paused gate, show the revision, evidence, cost, and requested decision. Use `approve_workflow_step` only for the user's explicit approve/reject choice. +6. For a paused gate, show the revision, evidence, cost, and requested decision. Use `approve_workflow_step` only for the user's explicit approve/reject choice, passing `decision` and the unchanged 40-character `expected_revision` returned by `get_workflow_run`. If the revision changed or is absent, stop and retrieve the run again instead of deciding the gate. 7. Use `cancel_workflow_run` when explicitly requested or when the documented safety boundary requires termination. 8. Finish with the exact revision, PR/result link, checks, evidence, cost, and any actionable terminal failure. @@ -46,4 +48,4 @@ Take one engineering issue to a verified, merge-ready pull request. Never merge - The result is merge-ready, not automatically merged or deployed. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change&utm_listing=github-skills&utm_platform=portable&utm_skill=implement-code-change).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change&utm_listing=github-skills&utm_platform=portable&utm_skill=implement-code-change) diff --git a/opencode/skills/judge-review-findings/SKILL.md b/opencode/skills/judge-review-findings/SKILL.md index d5ad8ae..9f30f17 100644 --- a/opencode/skills/judge-review-findings/SKILL.md +++ b/opencode/skills/judge-review-findings/SKILL.md @@ -41,4 +41,4 @@ For every `DEFER`, name the follow-up issue or return a complete follow-up draft - Do not apply a judgment to a different head revision. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings&utm_listing=github-skills&utm_platform=portable&utm_skill=judge-review-findings).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings&utm_listing=github-skills&utm_platform=portable&utm_skill=judge-review-findings) diff --git a/opencode/skills/reply-to-code-review/SKILL.md b/opencode/skills/reply-to-code-review/SKILL.md index a819360..8d2f8d0 100644 --- a/opencode/skills/reply-to-code-review/SKILL.md +++ b/opencode/skills/reply-to-code-review/SKILL.md @@ -42,4 +42,4 @@ Turn review feedback into verified remediation and concise, evidence-backed resp - Do not create endless automated reviewer loops. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=reply-to-code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=reply-to-code-review) diff --git a/opencode/skills/spec-review/SKILL.md b/opencode/skills/spec-review/SKILL.md index 09564e4..e809bfa 100644 --- a/opencode/skills/spec-review/SKILL.md +++ b/opencode/skills/spec-review/SKILL.md @@ -56,4 +56,4 @@ Decide whether an issue is ready to implement by comparing its claims and accept - If the issue targets a different revision or repository, stop and resolve the mismatch. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review&utm_listing=github-skills&utm_platform=portable&utm_skill=spec-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review&utm_listing=github-skills&utm_platform=portable&utm_skill=spec-review) diff --git a/package-lock.json b/package-lock.json index 588d11b..bc669a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,9 +24,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -41,9 +41,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -58,9 +58,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -75,9 +75,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -92,9 +92,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -109,9 +109,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -126,9 +126,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -143,9 +143,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -160,9 +160,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -177,9 +177,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -194,9 +194,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -211,9 +211,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -228,9 +228,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -245,9 +245,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -262,9 +262,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -279,9 +279,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -296,9 +296,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -313,9 +313,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -330,9 +330,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -347,9 +347,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -364,9 +364,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -381,9 +381,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -398,9 +398,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -415,9 +415,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -432,9 +432,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -449,9 +449,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -1192,15 +1192,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -1209,13 +1209,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.4", + "@vitest/spy": "3.2.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -1236,9 +1236,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", "dev": true, "license": "MIT", "dependencies": { @@ -1249,13 +1249,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.4", + "@vitest/utils": "3.2.7", "pathe": "^2.0.3", "strip-literal": "^3.0.0" }, @@ -1264,13 +1264,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", + "@vitest/pretty-format": "3.2.7", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -1279,9 +1279,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1292,13 +1292,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", + "@vitest/pretty-format": "3.2.7", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, @@ -1452,9 +1452,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1465,32 +1465,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/estree-walker": { @@ -1912,13 +1912,13 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", - "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -2010,20 +2010,20 @@ } }, "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", @@ -2053,8 +2053,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", "happy-dom": "*", "jsdom": "*" }, diff --git a/package.json b/package.json index ce67faf..41cced1 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,10 @@ "assemble:public": "node scripts/assemble-public-packages.mjs", "verify:public": "node scripts/assemble-public-packages.mjs --check", "validate:public": "npm run verify:public && node scripts/validate-public-packages.mjs", + "smoke:claude-public": "node scripts/smoke-claude-public.mjs", + "smoke:codex-public": "node scripts/smoke-codex-public.mjs", + "smoke:opencode-public": "node scripts/smoke-opencode-public.mjs", + "smoke:mcp-production": "node scripts/smoke-production-mcp.mjs", "prepublishOnly": "npm run build" }, "files": [ diff --git a/plugins/aictrl/.codex-plugin/plugin.json b/plugins/aictrl/.codex-plugin/plugin.json index c71e961..1b9d07b 100644 --- a/plugins/aictrl/.codex-plugin/plugin.json +++ b/plugins/aictrl/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "aictrl", - "version": "0.1.0-beta.1", + "version": "0.1.0-beta.2", "description": "Portable engineering skills with controlled AICtrl workflow execution", "author": { "name": "aictrl.dev", diff --git a/plugins/aictrl/.mcp.json b/plugins/aictrl/.mcp.json index bba34c4..544d452 100644 --- a/plugins/aictrl/.mcp.json +++ b/plugins/aictrl/.mcp.json @@ -2,7 +2,7 @@ "mcpServers": { "aictrl": { "type": "http", - "url": "https://aictrl.dev/mcp/workflows" + "url": "https://aictrl.dev/mcp" } } } diff --git a/plugins/aictrl/skills/code-review/SKILL.md b/plugins/aictrl/skills/code-review/SKILL.md index 5a09c15..6eaebb0 100644 --- a/plugins/aictrl/skills/code-review/SKILL.md +++ b/plugins/aictrl/skills/code-review/SKILL.md @@ -46,4 +46,4 @@ Produce high-signal findings for the exact pull or merge request revision. Revie - Separate verified defects from residual risk and untested hypotheses. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=code-review) diff --git a/plugins/aictrl/skills/create-bug/SKILL.md b/plugins/aictrl/skills/create-bug/SKILL.md index 17130bf..1c714be 100644 --- a/plugins/aictrl/skills/create-bug/SKILL.md +++ b/plugins/aictrl/skills/create-bug/SKILL.md @@ -61,4 +61,4 @@ Turn symptoms, logs, or an observed regression into a bug that another engineer - Do not implement the fix, commit, push, or change issue state unless the user asks. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug&utm_listing=github-skills&utm_platform=portable&utm_skill=create-bug).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug&utm_listing=github-skills&utm_platform=portable&utm_skill=create-bug) diff --git a/plugins/aictrl/skills/create-issue/SKILL.md b/plugins/aictrl/skills/create-issue/SKILL.md index 8e50dde..b26fa91 100644 --- a/plugins/aictrl/skills/create-issue/SKILL.md +++ b/plugins/aictrl/skills/create-issue/SKILL.md @@ -64,4 +64,4 @@ so that . - Never include secrets, credentials, private customer data, or unnecessary source excerpts. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue&utm_listing=github-skills&utm_platform=portable&utm_skill=create-issue).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue&utm_listing=github-skills&utm_platform=portable&utm_skill=create-issue) diff --git a/plugins/aictrl/skills/create-workflow/SKILL.md b/plugins/aictrl/skills/create-workflow/SKILL.md index 9eae3f0..00d56dd 100644 --- a/plugins/aictrl/skills/create-workflow/SKILL.md +++ b/plugins/aictrl/skills/create-workflow/SKILL.md @@ -5,7 +5,7 @@ description: Create and validate an AICtrl workflow v2 YAML file with typed para # Create an AICtrl Workflow -Author a reviewable `.aictrl/workflows/.yaml` file. Workflow v2 with inline `task` nodes is the default because it keeps task configuration portable in Git. +Author a reviewable `.aictrl/workflows/.yaml` file. Treat this directory like `.github/workflows/`: create the workflow in the repository whose automation the user is defining. Installing a skill or plugin does not publish a workflow for that repository. Workflow v2 with inline `task` nodes is the default because it keeps task configuration portable in Git. ## Workflow @@ -37,17 +37,18 @@ Author a reviewable `.aictrl/workflows/.yaml` file. Workflow v2 with schemaVersion: aictrl/workflow/v2 name: implement-change parameters: - - { name: repository, type: string, required: true } - - { name: issue-id, type: number, required: true } + - { name: repository, type: repository, required: true } + - { name: issue-id, type: number, required: true, validation: { min: 1 } } nodes: - id: implement type: task skill: implement-code-change@1.0.0 taskType: general prompt: Implement the requested issue and produce a merge-ready pull request. + timeoutMinutes: 10 parameters: - - { name: repository, type: string, required: true } - - { name: issue-id, type: number, required: true } + - { name: repository, type: repository, required: true } + - { name: issue-id, type: number, required: true, validation: { min: 1 } } inputs: repository: { from: input, name: repository } issue-id: { from: input, name: issue-id } @@ -64,4 +65,4 @@ nodes: - Canvas positions and runtime fields are platform-owned and must not be authored. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow&utm_listing=github-skills&utm_platform=portable&utm_skill=create-workflow).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow&utm_listing=github-skills&utm_platform=portable&utm_skill=create-workflow) diff --git a/plugins/aictrl/skills/create-workflow/reference/authoring-guide.md b/plugins/aictrl/skills/create-workflow/reference/authoring-guide.md index 3497c04..f9bffc1 100644 --- a/plugins/aictrl/skills/create-workflow/reference/authoring-guide.md +++ b/plugins/aictrl/skills/create-workflow/reference/authoring-guide.md @@ -39,7 +39,7 @@ triggers: [...] # optional; up to 10 file-declared event tri the platform on import. Authoring them is a schema error (`additionalProperties` is `false` at the top level and on every object). -## Parameter types (12 total) +## Parameter types (13 total) Used for workflow-level `parameters`, `user-input` node `parameters`, and inline `task` node `parameters`: @@ -54,6 +54,7 @@ Used for workflow-level `parameters`, `user-input` node `parameters`, and inline | `select` | string | Requires `options: [...]` | | `multi-select` | list | Requires `options: [...]` | | `json` | dyn | Arbitrary JSON | +| `repository` | string | Connected repository in owner/name form | | `pull-request` | string | PR URL | | `story` | string | Story/ticket URL | | `image` | string | Image URL or base64 | @@ -88,6 +89,7 @@ layer-1 error. skill: code-review@1.0.0 # required; pin when a version is resolvable taskType: code-review # required; executor tool boundary prompt: Review the current pull request head and return structured findings. + timeoutMinutes: 10 # optional; executor runtime, integer 1-60 parameters: - { name: pull-request, type: pull-request, required: true } inputs: @@ -101,6 +103,7 @@ Inline task nodes make the task configuration portable in the workflow file. Their `inputs` are checked against the in-file `parameters`; their declared `outputs` may be mapped by downstream nodes. Treat `prompt` as instructions for the selected skill, not as a way to relax workflow policy or tool boundaries. +`timeoutMinutes` is an optional hard executor bound from 1 to 60 minutes. ### `template` — run a task template ```yaml diff --git a/plugins/aictrl/skills/create-workflow/reference/v1/workflow.schema.json b/plugins/aictrl/skills/create-workflow/reference/v1/workflow.schema.json index 442541b..be88a9a 100644 --- a/plugins/aictrl/skills/create-workflow/reference/v1/workflow.schema.json +++ b/plugins/aictrl/skills/create-workflow/reference/v1/workflow.schema.json @@ -34,7 +34,7 @@ "name": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" }, "label": { "type": "string" }, "type": { - "enum": ["string", "text", "number", "boolean", "url", "select", "multi-select", "json", "pull-request", "story", "image", "github-issue"] + "enum": ["string", "text", "number", "boolean", "url", "select", "multi-select", "json", "repository", "pull-request", "story", "image", "github-issue"] }, "description": { "type": "string" }, "required": { "type": "boolean", "default": false }, diff --git a/plugins/aictrl/skills/create-workflow/reference/workflow.schema.json b/plugins/aictrl/skills/create-workflow/reference/workflow.schema.json index 0902b84..16fe7f0 100644 --- a/plugins/aictrl/skills/create-workflow/reference/workflow.schema.json +++ b/plugins/aictrl/skills/create-workflow/reference/workflow.schema.json @@ -87,8 +87,8 @@ "then": { "required": ["parameters"], "not": { "anyOf": [ { "required": ["template"] }, { "required": ["templateVersion"] }, { "required": ["workflow"] }, { "required": ["workflowVersion"] }, { "required": ["maxIterations"] }, { "required": ["until"] }, { "required": ["while"] }, { "required": ["onMaxIterations"] }, { "required": ["body"] }, { "required": ["signalSource"] }, { "required": ["timeoutMinutes"] }, { "required": ["checklistItems"] }, { "required": ["assignee"] }, { "required": ["skill"] }, { "required": ["taskType"] }, { "required": ["prompt"] }, { "required": ["outputs"] } ] } } }, { "if": { "properties": { "type": { "const": "task" } }, "required": ["type"] }, - "then": { "required": ["skill"], "not": { "anyOf": [ - { "required": ["template"] }, { "required": ["templateVersion"] }, { "required": ["workflow"] }, { "required": ["workflowVersion"] }, { "required": ["maxIterations"] }, { "required": ["until"] }, { "required": ["while"] }, { "required": ["onMaxIterations"] }, { "required": ["body"] }, { "required": ["signalSource"] }, { "required": ["timeoutMinutes"] }, { "required": ["checklistItems"] }, { "required": ["assignee"] } ] } } } + "then": { "required": ["skill"], "properties": { "timeoutMinutes": { "maximum": 60 } }, "not": { "anyOf": [ + { "required": ["template"] }, { "required": ["templateVersion"] }, { "required": ["workflow"] }, { "required": ["workflowVersion"] }, { "required": ["maxIterations"] }, { "required": ["until"] }, { "required": ["while"] }, { "required": ["onMaxIterations"] }, { "required": ["body"] }, { "required": ["signalSource"] }, { "required": ["checklistItems"] }, { "required": ["assignee"] } ] } } } ] }, "inputMapping": { "$ref": "../v1/workflow.schema.json#/$defs/inputMapping" }, diff --git a/plugins/aictrl/skills/implement-code-change/SKILL.md b/plugins/aictrl/skills/implement-code-change/SKILL.md index 4c18632..bd8e270 100644 --- a/plugins/aictrl/skills/implement-code-change/SKILL.md +++ b/plugins/aictrl/skills/implement-code-change/SKILL.md @@ -28,12 +28,14 @@ Take one engineering issue to a verified, merge-ready pull request. Never merge ## Connected workflow -1. Call `list_workflows` and confirm `implement-code-change` is available. -2. Call `get_workflow` and show the resolved version, required `{ repository, issue_id }` inputs, side effects, limits, and approval gates. +Connected workflows are repository-owned configuration, analogous to GitHub Actions. Installing this skill or its plugin does not provision one. If the organization has no suitable published workflow, explain that the user can invoke `create-workflow` in the repository they want to automate; do not create or publish a workflow without a separate request. + +1. Call `list_workflows` and confirm a suitable `implement-code-change` workflow is available. If it is absent, stop the connected path and offer the repository-owned `create-workflow` path. +2. Call `get_workflow` and show the resolved immutable version, required `{ repository, issue-id }` inputs, side effects, limits, and approval gates. Preserve the hyphenated `issue-id` key exactly in tool input. 3. Obtain explicit confirmation to start if the user has not already authorized connected execution. 4. Call `start_workflow` with the resolved workflow/version, validated inputs, and a stable idempotency key derived from repository and issue. 5. Poll with `get_workflow_run`; report actual status and required action without inventing progress. -6. For a paused gate, show the revision, evidence, cost, and requested decision. Use `approve_workflow_step` only for the user's explicit approve/reject choice. +6. For a paused gate, show the revision, evidence, cost, and requested decision. Use `approve_workflow_step` only for the user's explicit approve/reject choice, passing `decision` and the unchanged 40-character `expected_revision` returned by `get_workflow_run`. If the revision changed or is absent, stop and retrieve the run again instead of deciding the gate. 7. Use `cancel_workflow_run` when explicitly requested or when the documented safety boundary requires termination. 8. Finish with the exact revision, PR/result link, checks, evidence, cost, and any actionable terminal failure. @@ -46,4 +48,4 @@ Take one engineering issue to a verified, merge-ready pull request. Never merge - The result is merge-ready, not automatically merged or deployed. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change&utm_listing=github-skills&utm_platform=portable&utm_skill=implement-code-change).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change&utm_listing=github-skills&utm_platform=portable&utm_skill=implement-code-change) diff --git a/plugins/aictrl/skills/judge-review-findings/SKILL.md b/plugins/aictrl/skills/judge-review-findings/SKILL.md index d5ad8ae..9f30f17 100644 --- a/plugins/aictrl/skills/judge-review-findings/SKILL.md +++ b/plugins/aictrl/skills/judge-review-findings/SKILL.md @@ -41,4 +41,4 @@ For every `DEFER`, name the follow-up issue or return a complete follow-up draft - Do not apply a judgment to a different head revision. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings&utm_listing=github-skills&utm_platform=portable&utm_skill=judge-review-findings).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings&utm_listing=github-skills&utm_platform=portable&utm_skill=judge-review-findings) diff --git a/plugins/aictrl/skills/reply-to-code-review/SKILL.md b/plugins/aictrl/skills/reply-to-code-review/SKILL.md index a819360..8d2f8d0 100644 --- a/plugins/aictrl/skills/reply-to-code-review/SKILL.md +++ b/plugins/aictrl/skills/reply-to-code-review/SKILL.md @@ -42,4 +42,4 @@ Turn review feedback into verified remediation and concise, evidence-backed resp - Do not create endless automated reviewer loops. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=reply-to-code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=reply-to-code-review) diff --git a/plugins/aictrl/skills/spec-review/SKILL.md b/plugins/aictrl/skills/spec-review/SKILL.md index 09564e4..e809bfa 100644 --- a/plugins/aictrl/skills/spec-review/SKILL.md +++ b/plugins/aictrl/skills/spec-review/SKILL.md @@ -56,4 +56,4 @@ Decide whether an issue is ready to implement by comparing its claims and accept - If the issue targets a different revision or repository, stop and resolve the mismatch. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review&utm_listing=github-skills&utm_platform=portable&utm_skill=spec-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review&utm_listing=github-skills&utm_platform=portable&utm_skill=spec-review) diff --git a/public-skills.lock.json b/public-skills.lock.json index 5fc5b7f..b6cdcaf 100644 --- a/public-skills.lock.json +++ b/public-skills.lock.json @@ -1,9 +1,10 @@ { "schemaVersion": 1, "repository": "https://github.com/aictrl-dev/skills.git", - "commit": "99b618002daed84245cdc86f92abef811d803c8a", + "commit": "01cf2fa9b9bcf3b3b48cb00f3e48764c292ecf18", + "skillsVersion": "1.0.1", "checksumsFile": "CHECKSUMS.sha256", - "checksumsSha256": "f7ee4bd53e6ed2bacd68803dfb230b79a47649124fc5d1ecb0c1958be1a35a45", + "checksumsSha256": "a09b2075fbe54f2615aa8b8a2786162c1cdd1676e5944a000116786feda451b4", "sourceDirectory": "aictrl-skills/skills", "skills": [ "create-issue", diff --git a/scripts/assemble-public-packages.mjs b/scripts/assemble-public-packages.mjs index 569eb74..a13795b 100755 --- a/scripts/assemble-public-packages.mjs +++ b/scripts/assemble-public-packages.mjs @@ -9,6 +9,7 @@ import { readdirSync, rmSync, statSync, + writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join, relative, resolve } from 'node:path'; @@ -23,8 +24,12 @@ const requestedSource = sourceFlag === -1 ? null : process.argv[sourceFlag + 1]; if (sourceFlag !== -1 && !requestedSource) { throw new Error('--source requires a local checkout path'); } -if (lock.schemaVersion !== 1 || !/^[a-f0-9]{40}$/.test(lock.commit)) { - throw new Error('public-skills.lock.json has an unsupported schema or commit'); +if ( + lock.schemaVersion !== 1 + || !/^[a-f0-9]{40}$/.test(lock.commit) + || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(lock.skillsVersion) +) { + throw new Error('public-skills.lock.json has an unsupported schema, commit, or skills version'); } let cleanup = null; @@ -54,10 +59,13 @@ try { verifySource(sourceRoot); if (checkOnly) { verifyTargets(sourceRoot); + verifyDistributionMetadata(); console.log(`Verified ${lock.skills.length} pinned skills in ${lock.targets.length} package targets.`); } else { writeTargets(sourceRoot); + writeDistributionMetadata(); verifyTargets(sourceRoot); + verifyDistributionMetadata(); console.log(`Assembled ${lock.skills.length} pinned skills into ${lock.targets.length} package targets.`); } } finally { @@ -65,6 +73,21 @@ try { } function verifySource(source) { + const releaseTag = `v${lock.skillsVersion}`; + let releaseCommit; + try { + releaseCommit = execFileSync('git', ['rev-list', '-n', '1', releaseTag], { + cwd: source, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }).trim(); + } catch { + throw new Error(`Pinned skills release tag is missing: ${releaseTag}`); + } + if (releaseCommit !== lock.commit) { + throw new Error(`${releaseTag} resolves to ${releaseCommit}; lock requires ${lock.commit}`); + } + const checksumPath = join(source, lock.checksumsFile); const checksumBytes = readFileSync(checksumPath); if (sha256(checksumBytes) !== lock.checksumsSha256) { @@ -101,6 +124,71 @@ function verifySource(source) { } } +function writeDistributionMetadata() { + const codex = readJson('plugins/aictrl/.codex-plugin/plugin.json'); + const claude = readJson('claude/aictrl/.claude-plugin/plugin.json'); + const opencode = readJson('opencode/package.json'); + + writeJson('plugins/aictrl/.mcp.json', mcpConfig()); + writeJson('claude/aictrl/.mcp.json', mcpConfig()); + writeJson('opencode/skills-manifest.json', { + skillsVersion: lock.skillsVersion, + skills: lock.skills, + }); + + if (codex.version !== opencode.version || claude.version !== opencode.version) { + throw new Error('Claude, Codex, and OpenCode public package versions must match'); + } +} + +function verifyDistributionMetadata() { + const codex = readJson('plugins/aictrl/.codex-plugin/plugin.json'); + const claude = readJson('claude/aictrl/.claude-plugin/plugin.json'); + const opencode = readJson('opencode/package.json'); + const codexMcp = readJson('plugins/aictrl/.mcp.json'); + const claudeMcp = readJson('claude/aictrl/.mcp.json'); + const skillsManifest = readJson('opencode/skills-manifest.json'); + + if (codex.version !== opencode.version || claude.version !== opencode.version) { + throw new Error('Claude, Codex, and OpenCode public package versions must match'); + } + if (codexMcp.mcpServers?.aictrl?.url !== publicMcpUrl()) { + throw new Error('Codex MCP resource URL is not the canonical public workflow endpoint'); + } + if (claudeMcp.mcpServers?.aictrl?.url !== publicMcpUrl()) { + throw new Error('Claude MCP resource URL is not the canonical public workflow endpoint'); + } + if ( + skillsManifest.skillsVersion !== lock.skillsVersion + || JSON.stringify(skillsManifest.skills) !== JSON.stringify(lock.skills) + ) { + throw new Error('OpenCode skills manifest does not match the pinned skills lock'); + } +} + +function mcpConfig() { + return { + mcpServers: { + aictrl: { + type: 'http', + url: publicMcpUrl(), + }, + }, + }; +} + +function publicMcpUrl() { + return 'https://aictrl.dev/mcp'; +} + +function readJson(path) { + return JSON.parse(readFileSync(join(root, path), 'utf8')); +} + +function writeJson(path, value) { + writeFileSync(join(root, path), `${JSON.stringify(value, null, 2)}\n`); +} + function writeTargets(source) { for (const target of lock.targets) { const targetRoot = join(root, target); diff --git a/scripts/resolve-release-target.mjs b/scripts/resolve-release-target.mjs new file mode 100644 index 0000000..2c181a6 --- /dev/null +++ b/scripts/resolve-release-target.mjs @@ -0,0 +1,41 @@ +#!/usr/bin/env node +import { readFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const tag = process.argv[2]; + +const targets = [ + { + prefix: 'public-v', + packageSpec: './opencode', + packageJson: 'opencode/package.json', + distTag: 'beta', + }, + { + prefix: 'v', + packageSpec: '.', + packageJson: 'package.json', + distTag: 'latest', + }, +]; + +const target = targets.find((candidate) => tag?.startsWith(candidate.prefix)); +if (!target) { + fail(`Unsupported release tag: ${tag || '(missing)'}`); +} + +const packageMetadata = JSON.parse(readFileSync(join(root, target.packageJson), 'utf8')); +const expectedTag = `${target.prefix}${packageMetadata.version}`; +if (tag !== expectedTag) { + fail(`Release tag ${tag} does not match ${packageMetadata.name}@${packageMetadata.version}; expected ${expectedTag}`); +} + +process.stdout.write(`package_spec=${target.packageSpec}\n`); +process.stdout.write(`dist_tag=${target.distTag}\n`); + +function fail(message) { + console.error(message); + process.exit(1); +} diff --git a/scripts/smoke-claude-public.mjs b/scripts/smoke-claude-public.mjs new file mode 100644 index 0000000..84e4ad2 --- /dev/null +++ b/scripts/smoke-claude-public.mjs @@ -0,0 +1,127 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process'; +import { + existsSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const configDir = mkdtempSync(join(tmpdir(), 'aictrl-claude-public-')); +const workDir = mkdtempSync(join(tmpdir(), 'aictrl-claude-work-')); +const claude = process.env.CLAUDE_BIN || 'claude'; +const source = process.env.AICTRL_CLAUDE_MARKETPLACE_SOURCE || 'aictrl-dev/aictrl-plugin'; +const env = { ...process.env, CLAUDE_CONFIG_DIR: configDir }; +const marketplaceName = 'aictrl-public'; +const pluginId = `aictrl@${marketplaceName}`; +const publicMcpUrl = 'https://aictrl.dev/mcp'; +const expectedSkills = [ + 'code-review', + 'create-bug', + 'create-issue', + 'create-workflow', + 'implement-code-change', + 'judge-review-findings', + 'reply-to-code-review', + 'spec-review', +]; + +try { + run(['plugin', 'marketplace', 'add', source]); + const marketplaces = jsonOutput(['plugin', 'marketplace', 'list', '--json']); + const marketplace = marketplaces.find((entry) => entry.name === marketplaceName); + if (!marketplace || marketplace.source !== 'github') { + throw new Error(`Claude did not register ${marketplaceName} as a GitHub marketplace`); + } + + run(['plugin', 'install', pluginId, '--scope', 'user']); + assertInstalled(); + + // Re-installing the same public package must leave one enabled installation. + run(['plugin', 'install', pluginId, '--scope', 'user']); + assertInstalled(); + + run(['plugin', 'uninstall', pluginId, '--scope', 'user', '--yes']); + const afterUninstall = jsonOutput(['plugin', 'list', '--json']); + if (afterUninstall.some((plugin) => plugin.id === pluginId)) { + throw new Error('Claude uninstall left the AICtrl plugin installed'); + } + + run(['plugin', 'marketplace', 'remove', marketplaceName]); + const afterRemoval = jsonOutput(['plugin', 'marketplace', 'list', '--json']); + if (afterRemoval.some((entry) => entry.name === marketplaceName)) { + throw new Error('Claude marketplace removal left aictrl-public configured'); + } + + console.log('Claude public marketplace lifecycle smoke passed.'); +} finally { + rmSync(configDir, { recursive: true, force: true }); + rmSync(workDir, { recursive: true, force: true }); +} + +function assertInstalled() { + const installed = jsonOutput(['plugin', 'list', '--json']); + const matches = installed.filter((plugin) => plugin.id === pluginId); + if (matches.length !== 1 || !matches[0].enabled) { + throw new Error(`Expected one enabled ${pluginId} installation`); + } + + const installedRoot = resolve(matches[0].installPath); + if (!installedRoot.startsWith(`${resolve(configDir)}${sep}`)) { + throw new Error('Claude installed the plugin outside the isolated config directory'); + } + + const manifest = jsonFile(join(installedRoot, '.claude-plugin/plugin.json')); + if (manifest.name !== 'aictrl' || manifest.version !== matches[0].version) { + throw new Error('Installed Claude manifest does not match the plugin listing'); + } + + const mcp = jsonFile(join(installedRoot, '.mcp.json')); + if (mcp.mcpServers?.aictrl?.url !== publicMcpUrl) { + throw new Error('Installed Claude plugin does not target the canonical public workflow endpoint'); + } + + const skillsRoot = join(installedRoot, 'skills'); + const installedSkills = readdirSync(skillsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + if (JSON.stringify(installedSkills) !== JSON.stringify(expectedSkills)) { + throw new Error(`Installed Claude skills differ: ${installedSkills.join(', ')}`); + } + for (const skill of expectedSkills) { + if (!existsSync(join(skillsRoot, skill, 'SKILL.md'))) { + throw new Error(`Installed Claude skill is missing SKILL.md: ${skill}`); + } + } +} + +function run(args) { + try { + return execFileSync(claude, args, { + cwd: workDir, + env, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + } catch (error) { + const stderr = error?.stderr?.toString?.() || ''; + const stdout = error?.stdout?.toString?.() || ''; + const detail = stderr.trim() || stdout.trim(); + throw new Error(`claude ${args.join(' ')} failed${detail ? `: ${detail}` : ''}`); + } +} + +function jsonOutput(args) { + return JSON.parse(run(args)); +} + +function jsonFile(path) { + return JSON.parse(readFileSync(path, 'utf8')); +} diff --git a/scripts/smoke-codex-public.mjs b/scripts/smoke-codex-public.mjs new file mode 100644 index 0000000..47bdc0b --- /dev/null +++ b/scripts/smoke-codex-public.mjs @@ -0,0 +1,131 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process'; +import { + cpSync, + existsSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const codexHome = mkdtempSync(join(tmpdir(), 'aictrl-codex-public-')); +const marketplaceRoot = join(codexHome, 'marketplace'); +const marketplacePluginRoot = join(marketplaceRoot, 'plugins/aictrl'); +const codex = process.env.CODEX_BIN || 'codex'; +const env = { ...process.env, CODEX_HOME: codexHome }; +const pluginId = 'aictrl@aictrl-public'; +const publicMcpUrl = 'https://aictrl.dev/mcp'; +const expectedSkills = [ + 'code-review', + 'create-bug', + 'create-issue', + 'create-workflow', + 'implement-code-change', + 'judge-review-findings', + 'reply-to-code-review', + 'spec-review', +]; + +try { + cpSync(join(root, '.agents'), join(marketplaceRoot, '.agents'), { recursive: true }); + cpSync(join(root, 'plugins'), join(marketplaceRoot, 'plugins'), { recursive: true }); + + run(['plugin', 'marketplace', 'add', marketplaceRoot]); + assertIncludes(run(['plugin', 'marketplace', 'list']), 'aictrl-public', 'marketplace list'); + + const sourceManifest = json(join(marketplacePluginRoot, '.codex-plugin/plugin.json')); + run(['plugin', 'add', pluginId]); + assertInstalled(sourceManifest.version); + + // Re-installing the same package must be idempotent. + run(['plugin', 'add', pluginId]); + assertInstalled(sourceManifest.version); + + // A changed marketplace version must replace the installed cache entry. + const upgradeVersion = `${sourceManifest.version.split('-')[0]}-smoke.1`; + writeFileSync( + join(marketplacePluginRoot, '.codex-plugin/plugin.json'), + `${JSON.stringify({ ...sourceManifest, version: upgradeVersion }, null, 2)}\n`, + ); + run(['plugin', 'add', pluginId]); + assertInstalled(upgradeVersion); + + run(['plugin', 'remove', pluginId]); + assertIncludes(run(['plugin', 'list']), 'not installed', 'plugin list after removal'); + + const config = readFileSync(join(codexHome, 'config.toml'), 'utf8'); + if (config.includes(`[plugins."${pluginId}"]`)) { + throw new Error('Codex removal left the AICtrl plugin enabled in config.toml'); + } + assertIncludes(config, '[marketplaces.aictrl-public]', 'config after removal'); + + console.log('Codex public marketplace lifecycle smoke passed.'); +} finally { + rmSync(codexHome, { recursive: true, force: true }); +} + +function assertInstalled(expectedVersion) { + const listing = run(['plugin', 'list']); + assertIncludes(listing, pluginId, 'installed plugin list'); + assertIncludes(listing, 'installed, enabled', 'installed plugin status'); + + const installedRoot = join( + codexHome, + 'plugins/cache/aictrl-public/aictrl', + expectedVersion, + ); + const installedManifest = json(join(installedRoot, '.codex-plugin/plugin.json')); + if (installedManifest.name !== 'aictrl' || installedManifest.version !== expectedVersion) { + throw new Error('Installed Codex manifest does not match the source package'); + } + + const installedMcp = json(join(installedRoot, '.mcp.json')); + if (installedMcp.mcpServers?.aictrl?.url !== publicMcpUrl) { + throw new Error('Installed Codex plugin does not target the canonical public workflow endpoint'); + } + + const skillsRoot = join(installedRoot, 'skills'); + const installedSkills = readdirSync(skillsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + if (JSON.stringify(installedSkills) !== JSON.stringify(expectedSkills)) { + throw new Error(`Installed Codex skills differ: ${installedSkills.join(', ')}`); + } + for (const skill of expectedSkills) { + if (!existsSync(join(skillsRoot, skill, 'SKILL.md'))) { + throw new Error(`Installed Codex skill is missing SKILL.md: ${skill}`); + } + } +} + +function run(args) { + try { + return execFileSync(codex, args, { + cwd: root, + env, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + } catch (error) { + const stderr = error?.stderr?.toString?.() || ''; + throw new Error(`codex ${args.join(' ')} failed${stderr ? `: ${stderr.trim()}` : ''}`); + } +} + +function json(path) { + return JSON.parse(readFileSync(path, 'utf8')); +} + +function assertIncludes(value, expected, context) { + if (!value.includes(expected)) { + throw new Error(`${context} did not include ${JSON.stringify(expected)}`); + } +} diff --git a/scripts/smoke-opencode-public.mjs b/scripts/smoke-opencode-public.mjs new file mode 100644 index 0000000..a26606b --- /dev/null +++ b/scripts/smoke-opencode-public.mjs @@ -0,0 +1,159 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process'; +import { + existsSync, + mkdtempSync, + mkdirSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const tempRoot = mkdtempSync(join(tmpdir(), 'aictrl-opencode-public-')); +const packDir = join(tempRoot, 'pack'); +const configDir = join(tempRoot, 'config'); +const dataDir = join(tempRoot, 'data'); +const homeDir = join(tempRoot, 'home'); +const npm = process.env.NPM_BIN || 'npm'; +const opencode = process.env.OPENCODE_BIN || 'opencode'; +const productionMcpUrl = 'https://aictrl.dev/mcp'; +const connectivityOrigin = process.env.AICTRL_OPENCODE_CONNECTIVITY_ORIGIN || 'https://aictrl.dev'; +const connectivityUrl = new URL(new URL(productionMcpUrl).pathname, connectivityOrigin).toString(); +const configFile = join(configDir, 'opencode/opencode.json'); +const skillsRoot = join(configDir, 'opencode/skills'); +const env = { + ...process.env, + HOME: homeDir, + XDG_CONFIG_HOME: configDir, + XDG_DATA_HOME: dataDir, +}; +const expectedSkills = [ + 'code-review', + 'create-bug', + 'create-issue', + 'create-workflow', + 'implement-code-change', + 'judge-review-findings', + 'reply-to-code-review', + 'spec-review', +]; + +try { + mkdirSync(packDir, { recursive: true }); + const tarballName = run(npm, [ + 'pack', + './opencode', + '--pack-destination', + packDir, + '--silent', + ]).trim(); + if (!tarballName.endsWith('.tgz')) { + throw new Error(`npm pack returned an unexpected filename: ${tarballName}`); + } + const tarball = join(packDir, tarballName); + + install(tarball); + assertInstalled(productionMcpUrl); + + // Re-running the packed public command must be idempotent. + install(tarball); + assertInstalled(productionMcpUrl); + + if (connectivityUrl !== productionMcpUrl) { + const config = jsonFile(configFile); + config.mcp.aictrl.url = connectivityUrl; + writeFileSync(configFile, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 }); + } + + const listing = run(opencode, ['mcp', 'list']); + if (!listing.includes('aictrl') || !listing.includes('needs authentication')) { + throw new Error(`OpenCode did not reach the expected OAuth boundary:\n${listing.trim()}`); + } + if (listing.includes('failed')) { + throw new Error(`OpenCode reported a failed MCP connection:\n${listing.trim()}`); + } + + runPackage(tarball, ['--uninstall']); + const afterUninstall = jsonFile(configFile); + if (afterUninstall.mcp?.aictrl) { + throw new Error('OpenCode uninstall left the AICtrl MCP entry configured'); + } + for (const skill of expectedSkills) { + if (existsSync(join(skillsRoot, skill))) { + throw new Error(`OpenCode uninstall left an AICtrl skill installed: ${skill}`); + } + } + + console.log(`OpenCode public package lifecycle smoke passed against ${connectivityUrl}.`); +} finally { + rmSync(tempRoot, { recursive: true, force: true }); +} + +function install(tarball) { + runPackage(tarball, []); +} + +function runPackage(tarball, args) { + return run(npm, [ + 'exec', + '--yes', + '--package', + tarball, + '--', + 'aictrl-opencode', + ...args, + ]); +} + +function assertInstalled(expectedUrl) { + const config = jsonFile(configFile); + if (config.$schema !== 'https://opencode.ai/config.json') { + throw new Error('OpenCode package did not write the canonical schema URL'); + } + if ( + config.mcp?.aictrl?.type !== 'remote' + || config.mcp.aictrl.url !== expectedUrl + || config.mcp.aictrl.enabled !== true + ) { + throw new Error('OpenCode package did not write the production OAuth MCP entry'); + } + + const installedSkills = readdirSync(skillsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + if (JSON.stringify(installedSkills) !== JSON.stringify(expectedSkills)) { + throw new Error(`Installed OpenCode skills differ: ${installedSkills.join(', ')}`); + } + for (const skill of expectedSkills) { + if (!existsSync(join(skillsRoot, skill, 'SKILL.md'))) { + throw new Error(`Installed OpenCode skill is missing SKILL.md: ${skill}`); + } + } +} + +function run(command, args) { + try { + return execFileSync(command, args, { + cwd: root, + env, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + } catch (error) { + const stderr = error?.stderr?.toString?.() || ''; + const stdout = error?.stdout?.toString?.() || ''; + const detail = stderr.trim() || stdout.trim(); + throw new Error(`${command} ${args.join(' ')} failed${detail ? `: ${detail}` : ''}`); + } +} + +function jsonFile(path) { + return JSON.parse(readFileSync(path, 'utf8')); +} diff --git a/scripts/smoke-production-mcp.mjs b/scripts/smoke-production-mcp.mjs new file mode 100644 index 0000000..e140496 --- /dev/null +++ b/scripts/smoke-production-mcp.mjs @@ -0,0 +1,196 @@ +#!/usr/bin/env node + +import assert from 'node:assert/strict'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export const PRODUCTION_MCP_URL = 'https://aictrl.dev/mcp'; + +export const EXPECTED_CATALOG = [ + { + name: 'list_workflows', + annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, + inputSchema: { + type: 'object', + properties: { + organization_id: { type: 'string', minLength: 1 }, + limit: { type: 'integer', minimum: 1, maximum: 100 }, + }, + required: ['organization_id'], + additionalProperties: false, + }, + }, + { + name: 'get_workflow', + annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, + inputSchema: { + type: 'object', + properties: { + organization_id: { type: 'string', minLength: 1 }, + workflow_id: { type: 'string', minLength: 1 }, + }, + required: ['organization_id', 'workflow_id'], + additionalProperties: false, + }, + }, + { + name: 'start_workflow', + annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }, + inputSchema: { + type: 'object', + properties: { + organization_id: { type: 'string', minLength: 1 }, + workflow_id: { type: 'string', minLength: 1 }, + idempotency_key: { type: 'string', minLength: 8, maxLength: 200 }, + inputs: { type: 'object', additionalProperties: true }, + }, + required: ['organization_id', 'workflow_id', 'idempotency_key'], + additionalProperties: false, + }, + }, + { + name: 'get_workflow_run', + annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, + inputSchema: { + type: 'object', + properties: { + organization_id: { type: 'string', minLength: 1 }, + run_id: { type: 'string', minLength: 1 }, + }, + required: ['organization_id', 'run_id'], + additionalProperties: false, + }, + }, + { + name: 'approve_workflow_step', + annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }, + inputSchema: { + type: 'object', + properties: { + organization_id: { type: 'string', minLength: 1 }, + run_id: { type: 'string', minLength: 1 }, + decision: { type: 'string', enum: ['approve', 'reject'] }, + expected_revision: { type: 'string', pattern: '^[0-9a-fA-F]{40}$' }, + note: { type: 'string', maxLength: 2000 }, + }, + required: ['organization_id', 'run_id', 'decision', 'expected_revision'], + additionalProperties: false, + }, + }, + { + name: 'cancel_workflow_run', + annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }, + inputSchema: { + type: 'object', + properties: { + organization_id: { type: 'string', minLength: 1 }, + run_id: { type: 'string', minLength: 1 }, + reason: { type: 'string', maxLength: 2000 }, + }, + required: ['organization_id', 'run_id'], + additionalProperties: false, + }, + }, +]; + +export function normalizeCatalog(tools) { + if (!Array.isArray(tools)) { + throw new Error('Production MCP tools/list did not return a tools array.'); + } + + return tools.map((tool) => ({ + name: tool?.name, + annotations: tool?.annotations, + inputSchema: stripDescriptions(tool?.inputSchema), + })); +} + +export function assertProductionCatalog(tools) { + assert.deepStrictEqual( + normalizeCatalog(tools), + EXPECTED_CATALOG, + 'Production MCP catalog differs from the approved six-tool lifecycle contract.', + ); + + for (const tool of tools) { + if (typeof tool.description !== 'string' || tool.description.trim() === '') { + throw new Error(`Production MCP tool ${tool.name ?? ''} has no description.`); + } + if ( + typeof tool?._meta?.ui?.resourceUri === 'string' + || typeof tool?._meta?.['openai/outputTemplate'] === 'string' + ) { + throw new Error(`Production MCP tool ${tool.name ?? ''} unexpectedly links to custom UI.`); + } + } +} + +export async function scanProductionCatalog({ + apiKey, + url = PRODUCTION_MCP_URL, + fetchImpl = fetch, +} = {}) { + if (!apiKey) { + throw new Error('AICTRL_API_KEY is required for the authenticated production MCP scan.'); + } + + const response = await fetchImpl(url, { + method: 'POST', + redirect: 'error', + signal: AbortSignal.timeout(20_000), + headers: { + Accept: 'application/json, text/event-stream', + 'Content-Type': 'application/json', + 'MCP-Protocol-Version': '2024-11-05', + 'X-API-Key': apiKey, + }, + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }), + }); + + const body = await response.text(); + if (!response.ok) { + throw new Error(`Production MCP scan returned HTTP ${response.status}.`); + } + + const message = parseMcpResponse(body, response.headers.get('content-type')); + if (message?.error) { + throw new Error(`Production MCP tools/list returned JSON-RPC error ${message.error.code}.`); + } + assertProductionCatalog(message?.result?.tools); + return message.result.tools; +} + +function stripDescriptions(value) { + if (Array.isArray(value)) return value.map(stripDescriptions); + if (!value || typeof value !== 'object') return value; + + return Object.fromEntries( + Object.entries(value) + .filter(([key]) => key !== 'description') + .map(([key, child]) => [key, stripDescriptions(child)]), + ); +} + +function parseMcpResponse(body, contentType = '') { + if (!contentType?.includes('text/event-stream')) { + return JSON.parse(body); + } + + const messages = body + .split(/\r?\n/) + .filter((line) => line.startsWith('data:')) + .map((line) => JSON.parse(line.slice(5).trim())); + const response = messages.find((message) => message?.id === 1); + if (!response) throw new Error('Production MCP event stream contained no tools/list response.'); + return response; +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) { + try { + const tools = await scanProductionCatalog({ apiKey: process.env.AICTRL_API_KEY }); + console.log(`Production MCP catalog smoke passed: ${tools.map((tool) => tool.name).join(', ')}.`); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + } +} diff --git a/scripts/validate-public-packages.mjs b/scripts/validate-public-packages.mjs index 250a7ab..14ed5ea 100755 --- a/scripts/validate-public-packages.mjs +++ b/scripts/validate-public-packages.mjs @@ -9,13 +9,29 @@ const errors = []; const plugin = json('plugins/aictrl/.codex-plugin/plugin.json'); const marketplace = json('.agents/plugins/marketplace.json'); const mcp = json('plugins/aictrl/.mcp.json'); +const claudeMcp = json('claude/aictrl/.mcp.json'); const opencode = json('opencode/package.json'); +const opencodeSkills = json('opencode/skills-manifest.json'); +const claudePlugin = json('claude/aictrl/.claude-plugin/plugin.json'); +const claudeMarketplace = json('.claude-plugin/marketplace.json'); +const publicSkillsLock = json('public-skills.lock.json'); required(plugin, ['name', 'version', 'description', 'author', 'skills', 'mcpServers', 'interface']); if (plugin.name !== 'aictrl') errors.push('Codex plugin name must be aictrl'); if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(plugin.version)) { errors.push('Codex plugin version must be strict semver'); } +const publicVersion = opencode.version; +if (plugin.version !== publicVersion) { + errors.push('Codex and OpenCode public package versions must match'); +} +if (claudePlugin.version !== publicVersion) { + errors.push('Claude and OpenCode public package versions must match'); +} +const claudeEntry = claudeMarketplace.plugins?.find((candidate) => candidate.name === 'aictrl'); +if (claudeEntry?.version !== publicVersion) { + errors.push('Claude marketplace and public package versions must match'); +} for (const field of ['skills', 'mcpServers']) { const value = plugin[field]; if (typeof value !== 'string' || !value.startsWith('./')) errors.push(`${field} must be a ./ relative path`); @@ -52,16 +68,97 @@ else { if (typeof entry.category !== 'string' || !entry.category) errors.push('Codex marketplace category is required'); } -if (mcp.mcpServers?.aictrl?.url !== 'https://aictrl.dev/mcp/workflows') { - errors.push('Codex MCP must target the dedicated workflow endpoint'); +if ( + mcp.mcpServers?.aictrl?.url + !== publicMcpUrl() +) { + errors.push('Codex MCP must target the canonical public workflow endpoint'); +} +if ( + claudeMcp.mcpServers?.aictrl?.url + !== publicMcpUrl() +) { + errors.push('Claude MCP must target the canonical public workflow endpoint'); } if (opencode.name !== '@aictrl/opencode' || opencode.bin?.['aictrl-opencode'] !== 'bin/install.js') { errors.push('OpenCode npm package metadata is invalid'); } +if ( + opencodeSkills.skillsVersion !== publicSkillsLock.skillsVersion + || JSON.stringify(opencodeSkills.skills) !== JSON.stringify(publicSkillsLock.skills) +) { + errors.push('OpenCode skills manifest must match the pinned public skills release'); +} const testCases = readFileSync(join(root, 'submission/codex/test-cases.md'), 'utf8'); +const listing = readFileSync(join(root, 'submission/codex/listing.md'), 'utf8'); +for (const value of [ + plugin.interface?.displayName, + plugin.interface?.shortDescription, + plugin.interface?.longDescription, + plugin.interface?.developerName, + plugin.interface?.category, + plugin.interface?.websiteURL, + plugin.interface?.privacyPolicyURL, + plugin.interface?.termsOfServiceURL, + plugin.interface?.brandColor, + plugin.interface?.logo, + publicMcpUrl(), + 'https://aictrl.dev/support', +]) { + if (typeof value !== 'string' || !value || !listing.includes(`\`${value}\``)) { + errors.push(`Codex submission listing is missing the manifest-matched value: ${value}`); + } +} +for (const prompt of prompts || []) { + if (!listing.includes(`\`${prompt}\``)) { + errors.push(`Codex submission listing is missing starter prompt: ${prompt}`); + } +} +if (!listing.includes('| Browser fetch domains | None |')) { + errors.push('Codex submission listing must record that the no-custom-UI bundle has no browser fetch domains'); +} +if (existsSync(join(root, 'plugins/aictrl/.app.json'))) { + errors.push('Codex submission listing declares no custom UI, but plugins/aictrl/.app.json exists'); +} +const logoPath = join(root, 'plugins/aictrl', plugin.interface?.logo || '__missing_logo__'); +if (existsSync(logoPath)) { + const logo = readFileSync(logoPath, 'utf8'); + if (/<(?:script|foreignObject|text|filter)\b|(?:href|xlink:href)\s*=|url\s*\(/i.test(logo)) { + errors.push('Codex production logo must not contain script, external resources, embedded text, or filters'); + } +} +for (const fixturePath of [ + 'submission/codex/reviewer-fixture.md', + 'submission/codex/fixture-template/issue.md', + 'submission/codex/fixture-template/repository/LICENSE', + 'submission/codex/fixture-template/repository/package.json', + 'submission/codex/fixture-template/repository/src/labels.mjs', + 'submission/codex/fixture-template/repository/test/labels.test.mjs', +]) { + if (!existsSync(join(root, fixturePath))) errors.push(`Codex reviewer fixture file is required: ${fixturePath}`); +} if ((testCases.match(/^### P\d+ /gm) || []).length !== 5) errors.push('Codex reviewer pack must contain exactly five positive cases'); if ((testCases.match(/^### N\d+ /gm) || []).length !== 3) errors.push('Codex reviewer pack must contain exactly three negative cases'); +if ((testCases.match(/^- Fixture\/account:/gm) || []).length !== 5) { + errors.push('Every positive Codex reviewer case must declare fixture/account requirements'); +} +if (!testCases.includes('reviewer-fixture.md')) errors.push('Codex reviewer cases must reference the fixture specification'); +if (/\bissue_id\b/.test(testCases)) errors.push('Codex reviewer pack must use the canonical issue-id workflow input'); +if (!/\bissue-id\b/.test(testCases)) errors.push('Codex reviewer pack must exercise the canonical issue-id workflow input'); + +const opencodeEcosystem = readFileSync(join(root, 'submission/opencode/ecosystem.md'), 'utf8'); +for (const requiredText of [ + 'anomalyco/opencode', + 'packages/web/src/content/docs/ecosystem.mdx', + 'npm view @aictrl/opencode version dist-tags --json', + '[@aictrl/opencode](https://github.com/aictrl-dev/aictrl-plugin/tree/main/opencode)', + '### Issue for this PR', + '### Type of change', + 'Closes #', +]) { + if (!opencodeEcosystem.includes(requiredText)) errors.push(`OpenCode Ecosystem submission is missing: ${requiredText}`); +} for (const directory of ['claude', 'plugins', 'opencode', 'submission']) { for (const file of walk(join(root, directory))) { @@ -92,6 +189,10 @@ function required(object, fields) { for (const field of fields) if (object[field] == null) errors.push(`Codex manifest requires ${field}`); } +function publicMcpUrl() { + return 'https://aictrl.dev/mcp'; +} + function walk(directory) { const files = []; for (const entry of readdirSync(directory, { withFileTypes: true })) { diff --git a/scripts/verify-release-publication.mjs b/scripts/verify-release-publication.mjs new file mode 100644 index 0000000..4a2bf65 --- /dev/null +++ b/scripts/verify-release-publication.mjs @@ -0,0 +1,94 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { dirname, isAbsolute, relative, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const defaultRegistry = 'https://registry.npmjs.org/'; + +export async function inspectReleasePublication( + packageSpec, + { + fetchImpl = fetch, + pack = packPackage, + registry = process.env.NPM_CONFIG_REGISTRY || defaultRegistry, + } = {}, +) { + const packageDirectory = resolve(root, packageSpec); + const relativePackageDirectory = relative(root, packageDirectory); + if (relativePackageDirectory.startsWith('..') || isAbsolute(relativePackageDirectory)) { + throw new Error(`Package target must stay inside the repository: ${packageSpec}`); + } + + const packageMetadata = JSON.parse(readFileSync(resolve(packageDirectory, 'package.json'), 'utf8')); + const { name, version } = packageMetadata; + if (typeof name !== 'string' || typeof version !== 'string') { + throw new Error(`Package target is missing a valid name or version: ${packageSpec}`); + } + + const registryBase = registry.endsWith('/') ? registry : `${registry}/`; + const metadataUrl = `${registryBase}${encodeURIComponent(name)}/${encodeURIComponent(version)}`; + const response = await fetchImpl(metadataUrl, { + headers: { accept: 'application/json' }, + }); + + if (response.status === 404) { + return { published: false, name, version }; + } + if (!response.ok) { + throw new Error(`npm registry returned HTTP ${response.status} for ${name}@${version}`); + } + + const publishedMetadata = await response.json(); + const publishedIntegrity = publishedMetadata?.dist?.integrity; + if (typeof publishedIntegrity !== 'string' || publishedIntegrity.length === 0) { + throw new Error(`Published ${name}@${version} has no registry integrity`); + } + + const packed = await pack(packageSpec); + if (packed.name !== name || packed.version !== version || typeof packed.integrity !== 'string') { + throw new Error(`Local package metadata does not match ${name}@${version}`); + } + if (packed.integrity !== publishedIntegrity) { + throw new Error(`Published ${name}@${version} does not match the local release package`); + } + + return { published: true, name, version }; +} + +async function packPackage(packageSpec) { + const output = execFileSync('npm', ['pack', packageSpec, '--dry-run', '--json'], { + cwd: root, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + }); + const result = JSON.parse(output)?.[0]; + if (!result) { + throw new Error(`npm pack returned no package for ${packageSpec}`); + } + return result; +} + +async function main() { + const packageSpec = process.argv[2]; + if (!packageSpec) { + throw new Error('Usage: verify-release-publication.mjs '); + } + + const result = await inspectReleasePublication(packageSpec); + process.stdout.write(`published=${result.published}\n`); + process.stderr.write( + result.published + ? `${result.name}@${result.version} is already published with matching integrity.\n` + : `${result.name}@${result.version} is not yet published.\n`, + ); +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exit(1); + }); +} diff --git a/submission/codex/fixture-template/issue.md b/submission/codex/fixture-template/issue.md new file mode 100644 index 0000000..78615a2 --- /dev/null +++ b/submission/codex/fixture-template/issue.md @@ -0,0 +1,22 @@ +# Add deterministic GitHub label normalization + +## Context + +The fixture project can identify release labels, but callers also need a stable +way to normalize human-entered GitHub labels before comparison. + +## Acceptance criteria + +- Export `normalizeLabel(label)` from `src/labels.mjs`. +- Throw `TypeError` when `label` is not a string. +- Trim leading and trailing whitespace and lowercase ASCII letters. +- Treat each run of whitespace or underscores as one hyphen. +- Remove characters other than lowercase ASCII letters, digits, and hyphens. +- Collapse repeated hyphens and remove leading or trailing hyphens. +- Return an empty string when no valid characters remain. +- Preserve the existing `isReleaseLabel` behavior. +- Add deterministic unit coverage for normal text, repeated separators, + punctuation, an all-invalid value, and a non-string input. +- Keep the project dependency-free and make `npm test` pass. + +The result must stop at a merge-ready pull request. Do not merge or deploy it. diff --git a/submission/codex/fixture-template/repository/LICENSE b/submission/codex/fixture-template/repository/LICENSE new file mode 100644 index 0000000..68ff8cb --- /dev/null +++ b/submission/codex/fixture-template/repository/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 aictrl.dev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/submission/codex/fixture-template/repository/README.md b/submission/codex/fixture-template/repository/README.md new file mode 100644 index 0000000..1246d29 --- /dev/null +++ b/submission/codex/fixture-template/repository/README.md @@ -0,0 +1,16 @@ +# AICtrl plugin reviewer fixture + +This dependency-free repository is the deterministic baseline for AICtrl's +public plugin review cases. It intentionally contains one small JavaScript +module and a fast Node test suite. + +```bash +npm test +``` + +The repository must remain public and contain no credentials, customer data, +private dependencies, production integrations, deployment configuration, or +organization-only instructions. Reviewer workflows may create feature branches +and pull requests, but must never merge or deploy them automatically. + +Released under the MIT License. diff --git a/submission/codex/fixture-template/repository/package.json b/submission/codex/fixture-template/repository/package.json new file mode 100644 index 0000000..debc6c8 --- /dev/null +++ b/submission/codex/fixture-template/repository/package.json @@ -0,0 +1,12 @@ +{ + "name": "aictrl-plugin-reviewer-fixture", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "test": "node --test" + }, + "engines": { + "node": ">=20" + } +} diff --git a/submission/codex/fixture-template/repository/src/labels.mjs b/submission/codex/fixture-template/repository/src/labels.mjs new file mode 100644 index 0000000..6521e43 --- /dev/null +++ b/submission/codex/fixture-template/repository/src/labels.mjs @@ -0,0 +1,4 @@ +/** Return true when a GitHub label belongs to the release namespace. */ +export function isReleaseLabel(label) { + return typeof label === 'string' && /^release:/i.test(label.trim()); +} diff --git a/submission/codex/fixture-template/repository/test/labels.test.mjs b/submission/codex/fixture-template/repository/test/labels.test.mjs new file mode 100644 index 0000000..68740b6 --- /dev/null +++ b/submission/codex/fixture-template/repository/test/labels.test.mjs @@ -0,0 +1,14 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { isReleaseLabel } from '../src/labels.mjs'; + +test('recognizes release labels without depending on letter case', () => { + assert.equal(isReleaseLabel('release:beta'), true); + assert.equal(isReleaseLabel(' RELEASE:stable'), true); +}); + +test('rejects unrelated and non-string labels', () => { + assert.equal(isReleaseLabel('documentation'), false); + assert.equal(isReleaseLabel(null), false); +}); diff --git a/submission/codex/listing.md b/submission/codex/listing.md new file mode 100644 index 0000000..6d74ece --- /dev/null +++ b/submission/codex/listing.md @@ -0,0 +1,91 @@ +# Codex plugin listing + +Use these values in the OpenAI plugin submission portal. They mirror the +published package manifest so the portal listing and install surface do not +drift. Do not submit while any owner-only item below remains unresolved. + +## Info + +| Portal field | Value | +| --- | --- | +| Plugin name | `AICtrl` | +| Short description | `Engineering skills and controlled workflows` | +| Long description | `Use eight portable engineering skills locally, then hand implementation work to versioned AICtrl workflows with approvals, evidence, history, and policy controls.` | +| Category | `Productivity` | +| Developer name | `aictrl.dev` | +| Website | `https://aictrl.dev` | +| Support | `https://aictrl.dev/support` | +| Privacy policy | `https://aictrl.dev/privacy` | +| Terms of service | `https://aictrl.dev/terms` | +| Brand color | `#4c6ef5` | +| Logo | `./assets/icon.svg` (source: `plugins/aictrl/assets/icon.svg`) | + +The bundled logo is the canonical 64-unit indigo hex mark from the AICtrl +brand system. It has no script, external resource, embedded text, filter, +shadow, or background-dependent color. The release owner must still approve +the portal-rendered production preview before submission. + +Select the verified `aictrl.dev` business identity in the portal. Stop if that +identity is absent, does not match the public URLs above, or the submitter lacks +Apps Management write access. + +## MCP + +| Portal field | Value | +| --- | --- | +| Production MCP URL | `https://aictrl.dev/mcp` | +| Authentication | OAuth 2.1 | +| Custom UI | None | +| Browser fetch domains | None | +| Content-security-policy allowlists | Empty | + +The package contains no `.app.json`, web component, iframe, browser script, or +browser-side fetch. Do not add `aictrl.dev` or unrelated analytics domains to +the CSP merely because the MCP server or website uses them. The MCP URL and +domain challenge are separate portal fields. Stop if the portal scan discovers +a browser-fetch dependency; document that dependency and add only its exact +origin before continuing. + +After the portal provides the domain challenge, follow +`docs/public-release-runbook.md`: temporarily configure the exact value on the +production Cloud Run service, verify the well-known endpoint, and never place +the value in Git, CI, Terraform, Secret Manager, shell history, or release +evidence. + +Scan the production server and require exactly these tools: + +1. `list_workflows` +2. `get_workflow` +3. `start_workflow` +4. `get_workflow_run` +5. `approve_workflow_step` +6. `cancel_workflow_run` + +The protected-key CI scan is the source of truth for tool schemas, safety +annotations, and non-empty descriptions. + +## Skills and prompts + +Upload the final `plugins/aictrl/skills/` tree from the release commit. Use +exactly these three starter prompts from the manifest without adding portal-only +variants: + +1. `Turn this request into an implementation-ready issue.` +2. `Implement this issue and prepare a merge-ready pull request.` +3. `Review this pull request at its current head revision.` + +Upload the five positive and three negative cases from `test-cases.md` only +after every connected positive case contains the rehearsed fixture, account, +and run data described in `reviewer-fixture.md`. + +## Global and submit + +The release owner selects only countries or regions covered by current product, +support, privacy, and legal commitments. Record that decision in +`readiness.md`; do not infer availability from website reachability. + +Use `release-notes.md` for the initial-submission notes. Complete policy +attestations only after the final listing, server scan, skills, prompts, tests, +demo account, and availability decision are all accurate. Submission starts +review; after approval, the owner must explicitly publish and complete the +universal-directory smoke test. diff --git a/submission/codex/readiness.md b/submission/codex/readiness.md index 973a72e..60f31e0 100644 --- a/submission/codex/readiness.md +++ b/submission/codex/readiness.md @@ -4,26 +4,56 @@ - [x] `.codex-plugin/plugin.json` passes local ingestion validation. - [x] Marketplace policy includes installation, authentication, and category. +- [x] Codex CLI 0.144.4 completes clean marketplace add, install, repeat install, + version upgrade, and removal in CI while preserving marketplace configuration. +- [x] Claude Code 2.1.210 completes clean public marketplace add, install, repeat + install, and removal in CI. +- [x] OpenCode 1.18.1 installs the packed npm artifact, discovers the canonical + sandbox OAuth boundary, repeats idempotently, and removes only + AICtrl-managed state after aictrl-dev/aictrl#3904 is deployed. - [x] Eight skills are byte-pinned and checksum-verified. - [x] Website, support, privacy, and terms URLs return HTTP 200. - [x] Starter prompts are limited to three. - [x] Reviewer pack contains exactly five positive and three negative cases. -- [ ] Final logo and screenshots are approved for the public listing. +- [x] The dedicated public reviewer repository, protected baseline, and bounded + fixture issue described in `reviewer-fixture.md` are provisioned. +- [ ] The repository-owned fixture workflow in + `aictrl-dev/aictrl-plugin-reviewer-fixture#2` is independently reviewed and + merged through protected `main`. +- [ ] Every positive connected reviewer case names the rehearsed no-MFA demo + account and run data; the AICtrl connection and account remain pending. +- [x] `listing.md` records the manifest-matched portal copy and canonical AICtrl + logo asset; the SVG has no script, external resource, embedded text, + filter, shadow, or background-dependent color. +- [ ] The release owner approves the logo as rendered in the final portal + preview. Screenshots are optional in the current submission guidance and + are not a release gate. ## MCP and OAuth -- [ ] `https://aictrl.dev/mcp/workflows` is deployed and publicly reachable. -- [ ] Tool scan returns exactly the six workflow lifecycle tools. -- [ ] Schemas and all three annotations match deployed behavior. +- [x] The canonical resource URL from `plugins/aictrl/.mcp.json` is deployed and + publicly reachable. +- [x] The protected-key production scan returns exactly the six workflow + lifecycle tools from `https://aictrl.dev/mcp` (CI run `29420053568`, job + `87367910414`). +- [x] The same production scan matches the approved input schemas, all three + safety annotations, and non-empty tool descriptions. - [ ] Dynamic registration, PKCE, client/redirect binding, replay, refresh, and cancellation negative tests pass from a clean Codex client. +- [x] `listing.md` records empty browser content-security-policy allowlists: the + final package has no `.app.json`, custom UI, iframe, browser script, or + browser-side fetch. +- [ ] The final portal scan accepts those empty allowlists and does not identify + a browser-fetch dependency. - [ ] Domain challenge is installed at the portal-provided well-known URL. - [ ] Reviewer account works without MFA, email confirmation, SMS, or private network. ## Publisher and publication -- [ ] Publisher identity is verified in the owning OpenAI organization. -- [ ] Submitter has Apps Management write permission. +- [x] The release owner confirmed on 2026-07-15 that the publisher identity is + verified in the owning OpenAI organization. +- [x] The release owner confirmed on 2026-07-15 that the submitter has Apps + Management write permission in that same organization. - [ ] Availability regions and policy attestations are approved. - [ ] Plugin is submitted, approved, explicitly published, and smoke-tested from the universal plugin directory. diff --git a/submission/codex/reviewer-fixture.md b/submission/codex/reviewer-fixture.md new file mode 100644 index 0000000..c384c5c --- /dev/null +++ b/submission/codex/reviewer-fixture.md @@ -0,0 +1,91 @@ +# Codex reviewer fixture + +Provision this fixture before replacing the placeholders in `test-cases.md` or +submitting the plugin. Do not use an AICtrl production backlog repository as a +review fixture. + +The deterministic repository seed is in `fixture-template/repository/`, and the +disposable issue body is in `fixture-template/issue.md`. Keep them synchronized +with the final provisioned resources. + +## Provisioned GitHub fixture + +- Repository: [`aictrl-dev/aictrl-plugin-reviewer-fixture`](https://github.com/aictrl-dev/aictrl-plugin-reviewer-fixture) +- Baseline revision: `09b5d36ae163a39fe6b3f56ce347a8cb026afd2c` +- Fixture issue: [`aictrl-dev/aictrl-plugin-reviewer-fixture#1`](https://github.com/aictrl-dev/aictrl-plugin-reviewer-fixture/issues/1) +- Repository-owned workflow: [`aictrl-dev/aictrl-plugin-reviewer-fixture#2`](https://github.com/aictrl-dev/aictrl-plugin-reviewer-fixture/pull/2), + schema/DAG-valid at `77dc71a2e0cd857e09c0fe56055eb0db95ff4961` + and pending the required independent approval and merge. +- Baseline verification: dependency-free `npm test` passes two tests. +- Default branch: `main`, protected with one approval, stale-review dismissal, + last-push approval, conversation resolution, and admin enforcement. Force + pushes, branch deletion, merge commits, and automatic merge are disabled. + +The AICtrl repository connection and no-MFA portal demo account remain pending. +Do not replace the connected test-case placeholders or submit the reviewer pack +until those resources are provisioned and the clean-client rehearsal passes. + +## Required external resources + +- [x] A dedicated public GitHub repository owned by `aictrl-dev`. +- [x] One small, deterministic project with a fast test command and no credentials, + customer data, private dependencies, or organization-only instructions. +- [x] One open, disposable issue that requests a bounded code change with explicit + acceptance criteria. The issue must be safe to run repeatedly. +- [x] Default-branch rules that reject force-pushes and direct workflow writes while + still allowing the GitHub integration to create feature branches and pull + requests. The connected workflow must not receive merge permission. +- [ ] The repository-owned `implement-code-change` workflow is independently + reviewed and merged through the protected default branch. +- [ ] An active AICtrl repository connection for the reviewer organization and only + the fixture repository. +- [ ] A portal demo account that can complete OAuth and the connected cases without + MFA, email confirmation, SMS, private-network access, or support intervention. + +Never commit the demo password, recovery code, OAuth token, GitHub installation +token, domain challenge, or reviewer session to this repository or release +evidence. + +## Repeatability contract + +Before every reviewer run: + +1. Restore the fixture default branch to the documented baseline through a + normal reviewed change; never rewrite public release history. +2. Close or label prior generated pull requests so the next result is + unambiguous. Preserve old runs and pull requests as audit evidence. +3. Confirm the fixture issue is open and still matches the baseline. +4. Confirm the reviewer account can see only the intended AICtrl organization + and repository connection. +5. Record the default-branch commit SHA and UTC start time outside the prompt. + +The connected workflow must validate that exact revision, pause before any +write, and create or update one merge-ready pull request without merging or +deploying. P4 uses the paused run from P3. P5 uses a separate fresh run so the +cancellation case cannot invalidate the approval case. + +## Provisioning verification + +The release owner approved and provisioned the repository from the reviewed +`fixture-template/repository/` tree and created its fixture issue from +`fixture-template/issue.md`. + +Run these read-only checks after the owner provisions the resources: + +```bash +gh repo view aictrl-dev/aictrl-plugin-reviewer-fixture \ + --json nameWithOwner,visibility,defaultBranchRef,url +gh issue view 1 --repo aictrl-dev/aictrl-plugin-reviewer-fixture \ + --json number,title,state,url +``` + +Then complete a clean-client rehearsal and record only non-secret evidence: + +- final repository and issue URLs; +- package, skill, and workflow versions; +- OAuth start/completion/cancellation outcomes; +- paused and approved exact revisions; +- terminal run status and generated pull-request URL; +- redacted evidence, checks, reported cost, and elapsed time. + +Replace every placeholder in `test-cases.md` only after this rehearsal passes. diff --git a/submission/codex/test-cases.md b/submission/codex/test-cases.md index 480a787..906bc82 100644 --- a/submission/codex/test-cases.md +++ b/submission/codex/test-cases.md @@ -2,12 +2,16 @@ Run these cases from the final `plugins/aictrl` file tree. The submission must contain exactly these five positive and three negative cases. +Provision and rehearse the connected resources in `reviewer-fixture.md` before +replacing any placeholder or submitting this pack. ## Positive cases ### P1 — Create an issue locally before authentication - Prompt: `Turn this feature request into an implementation-ready issue: add CSV export to the audit page.` +- Fixture/account: no AICtrl account. Use a clean checkout of the public + `aictrl-dev/aictrl-plugin` repository at tag `v2.2.1`. - Expected behavior: loads `create-issue`, inspects the fixture repository, and drafts scope, acceptance criteria, tests, risks, and open questions. - Expected result: provider-neutral Markdown or an explicitly authorized provider @@ -15,7 +19,10 @@ contain exactly these five positive and three negative cases. ### P2 — Review the exact pull-request head locally -- Prompt: `Review pull request 42 at its current head. Do not change code.` +- Prompt: `Review aictrl-dev/aictrl-plugin pull request 17 at its current head. Do not change code.` +- Fixture/account: no AICtrl account. Public merged pull request + `https://github.com/aictrl-dev/aictrl-plugin/pull/17`, whose head commit is + `a29d09f9542f32ca49f1e60a54b36ea235d2c048`. - Expected behavior: loads `code-review`, records the exact head SHA, inspects changed and surrounding code, and reports only evidence-backed findings. - Expected result: structured findings bound to the SHA; no edits, push, merge, @@ -23,7 +30,11 @@ contain exactly these five positive and three negative cases. ### P3 — Start connected implementation with native OAuth -- Prompt: `Hand issue 17 in aictrl-dev/reviewer-fixture to the connected implement-code-change workflow.` +- Prompt: `Hand issue in / to the connected implement-code-change workflow.` +- Fixture/account: pending before submission. Record the portal demo account and + a dedicated public repository/issue that is connected to its AICtrl + organization. Do not substitute a production backlog issue. This case is not + reviewer-ready while these placeholders remain. - Expected behavior: calls `list_workflows`, then `get_workflow`; shows resolved version, inputs, side effects, limits, and gates; starts native OAuth; calls `start_workflow` once with a stable idempotency key after authorization. @@ -33,6 +44,8 @@ contain exactly these five positive and three negative cases. ### P4 — Approve a paused workflow gate - Prompt: `Show me the evidence for the paused gate, then approve it.` +- Fixture/account: use the portal demo account and paused run created by P3. The + run must expose the fixture repository's exact 40-character base revision. - Expected behavior: calls `get_workflow_run`, presents exact revision, evidence, cost, and requested action, then calls `approve_workflow_step` for that run and revision. @@ -42,6 +55,8 @@ contain exactly these five positive and three negative cases. ### P5 — Cancel and inspect an active run - Prompt: `Cancel the active implementation run and show the retained result.` +- Fixture/account: use the portal demo account and a fresh active run for the P3 + fixture issue; do not cancel the run used to verify P4 approval. - Expected behavior: confirms the target run, calls `cancel_workflow_run`, then calls `get_workflow_run`. - Expected result: cancelled terminal state with redacted retained evidence and @@ -66,7 +81,7 @@ contain exactly these five positive and three negative cases. ### N3 — Malformed connected inputs -- Prompt: `Start implement-code-change with repository "../secrets" and issue_id "all".` +- Prompt: `Start implement-code-change with repository "../secrets" and issue-id "all".` - Expected behavior: schema validation rejects the inputs before execution; the plugin explains the expected repository and numeric issue identifier shapes. - Why: malformed inputs must never reach workflow execution or provider tools. diff --git a/submission/opencode/ecosystem.md b/submission/opencode/ecosystem.md new file mode 100644 index 0000000..91cac4c --- /dev/null +++ b/submission/opencode/ecosystem.md @@ -0,0 +1,108 @@ +# OpenCode Ecosystem submission + +Do not open the upstream issue or pull request until `@aictrl/opencode` is +publicly installable and the exact published version passes the clean lifecycle +smoke. Submission is not publication; keep aictrl-dev/aictrl#3865 open until the +upstream PR is merged and the live Ecosystem link is smoke-tested. + +## Upstream target + +- Repository: `anomalyco/opencode` +- Base branch: `dev` +- File: `packages/web/src/content/docs/ecosystem.mdx` +- Section: `Plugins` + +The upstream contribution policy requires an issue before every PR. Use its +feature-request form and keep both descriptions short. + +## Preconditions + +```bash +npm view @aictrl/opencode version dist-tags --json +npx @aictrl/opencode --project . +opencode mcp list +npx @aictrl/opencode --project . --uninstall +``` + +Record the public version, `beta` dist-tag, successful clean install, canonical +MCP URL, OAuth-required boundary, uninstall result, and UTC time. + +## Upstream issue + +Title: + +```text +[FEATURE]: List @aictrl/opencode in the Ecosystem +``` + +Body: + +```markdown +### Feature hasn't been suggested before + +- [x] I have verified this feature I'm about to request hasn't been suggested before. + +### Describe the enhancement you want to request + +Add `@aictrl/opencode` to the Plugins table in `packages/web/src/content/docs/ecosystem.mdx`. It gives OpenCode users eight local SDLC skills plus an optional OAuth-connected AICtrl workflow MCP. The npm package is public and its clean install, MCP discovery, repeat install, and uninstall lifecycle has been verified. +``` + +Search open and closed upstream issues immediately before creating this issue; +reuse an existing matching issue instead of creating a duplicate. + +## One-row documentation change + +Append this row to the English `Plugins` table only, matching recent accepted +Ecosystem additions: + +```markdown +| [@aictrl/opencode](https://github.com/aictrl-dev/aictrl-plugin/tree/main/opencode) | Install eight portable SDLC skills and optional OAuth-connected AICtrl workflows | +``` + +PR title: + +```text +docs: add AICtrl to the OpenCode ecosystem +``` + +PR body: + +```markdown +### Issue for this PR + +Closes # + +### Type of change + +- [ ] Bug fix +- [ ] New feature +- [ ] Refactor / code improvement +- [x] Documentation + +### What does this PR do? + +Adds the public `@aictrl/opencode` package to the Plugins table. It installs eight portable SDLC skills and an optional OAuth-connected workflow MCP. + +### How did you verify your code works? + +Public npm install, MCP discovery, repeat install, and uninstall passed for version . The PR changes only the English Ecosystem table. + +### Screenshots / recordings + +N/A — documentation-only change. + +### Checklist + +- [x] I have tested my changes locally +- [x] I have not included unrelated changes in this PR +``` + +Do not expand the PR beyond the one English Ecosystem row unless an upstream +maintainer explicitly requests translated pages or other changes. + +## After merge + +Verify the live Ecosystem page links to the public repository, install the npm +package from a clean OpenCode client, complete one local skill, complete the +connected OAuth workflow smoke, and record the upstream issue, PR, merge commit, +live URL, package version, and evidence on aictrl-dev/aictrl#3865. diff --git a/test/codex-reviewer-fixture.test.ts b/test/codex-reviewer-fixture.test.ts new file mode 100644 index 0000000..7681403 --- /dev/null +++ b/test/codex-reviewer-fixture.test.ts @@ -0,0 +1,42 @@ +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const fixtureRoot = join(root, 'submission/codex/fixture-template'); +const repositoryRoot = join(fixtureRoot, 'repository'); + +describe('Codex reviewer fixture template', () => { + it('is a dependency-free private package with a passing baseline test', () => { + const packageJson = JSON.parse(readFileSync(join(repositoryRoot, 'package.json'), 'utf8')); + + expect(packageJson).toMatchObject({ + name: 'aictrl-plugin-reviewer-fixture', + private: true, + type: 'module', + scripts: { test: 'node --test' }, + }); + expect(packageJson.dependencies).toBeUndefined(); + expect(packageJson.devDependencies).toBeUndefined(); + + const output = execFileSync(process.execPath, ['--test'], { + cwd: repositoryRoot, + encoding: 'utf8', + }); + expect(output).toContain('# pass 2'); + expect(output).toContain('# fail 0'); + }); + + it('keeps the requested change absent from baseline and fully specified', () => { + const source = readFileSync(join(repositoryRoot, 'src/labels.mjs'), 'utf8'); + const issue = readFileSync(join(fixtureRoot, 'issue.md'), 'utf8'); + + expect(source).not.toContain('normalizeLabel'); + expect(issue).toContain('Export `normalizeLabel(label)`'); + expect(issue).toContain('Throw `TypeError`'); + expect(issue).toContain('Keep the project dependency-free'); + expect(issue).toContain('Do not merge or deploy'); + }); +}); diff --git a/test/production-mcp-catalog.test.ts b/test/production-mcp-catalog.test.ts new file mode 100644 index 0000000..f6c3b05 --- /dev/null +++ b/test/production-mcp-catalog.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it, vi } from 'vitest'; +import { + EXPECTED_CATALOG, + assertProductionCatalog, + scanProductionCatalog, +} from '../scripts/smoke-production-mcp.mjs'; + +function liveShape() { + return EXPECTED_CATALOG.map((tool) => ({ + ...tool, + annotations: { ...tool.annotations }, + description: `${tool.name} description`, + inputSchema: addDescriptions(tool.inputSchema), + })); +} + +function addDescriptions(value: unknown): unknown { + if (Array.isArray(value)) return value.map(addDescriptions); + if (!value || typeof value !== 'object') return value; + return { + description: 'ignored contract documentation', + ...Object.fromEntries( + Object.entries(value).map(([key, child]) => [key, addDescriptions(child)]), + ), + }; +} + +describe('production MCP catalog smoke', () => { + it('accepts exactly the six approved tools while ignoring descriptions', () => { + expect(() => assertProductionCatalog(liveShape())).not.toThrow(); + }); + + it('rejects extra tools, contract drift, missing descriptions, and custom UI', () => { + const extra = [...liveShape(), liveShape()[0]]; + expect(() => assertProductionCatalog(extra)).toThrow(/differs/); + + const schemaDrift = liveShape(); + schemaDrift[2].inputSchema.properties.idempotency_key.minLength = 1; + expect(() => assertProductionCatalog(schemaDrift)).toThrow(/differs/); + + const annotationDrift = liveShape(); + annotationDrift[4].annotations.destructiveHint = false; + expect(() => assertProductionCatalog(annotationDrift)).toThrow(/differs/); + + const missingDescription = liveShape(); + missingDescription[0].description = ''; + expect(() => assertProductionCatalog(missingDescription)).toThrow(/has no description/); + + const standardUi = liveShape(); + standardUi[0]._meta = { ui: { resourceUri: 'ui://aictrl/workflows.html' } }; + expect(() => assertProductionCatalog(standardUi)).toThrow(/unexpectedly links to custom UI/); + + const compatibilityUi = liveShape(); + compatibilityUi[0]._meta = { 'openai/outputTemplate': 'ui://aictrl/workflows.html' }; + expect(() => assertProductionCatalog(compatibilityUi)).toThrow(/unexpectedly links to custom UI/); + }); + + it('uses API-key authentication and accepts an MCP event-stream response', async () => { + const tools = liveShape(); + const fetchImpl = vi.fn(async (_url: string, init: RequestInit) => { + expect(init.headers).toMatchObject({ 'X-API-Key': 'secret-key' }); + expect(String(init.body)).not.toContain('secret-key'); + return new Response(`event: message\ndata: ${JSON.stringify({ + jsonrpc: '2.0', id: 1, result: { tools }, + })}\n\n`, { status: 200, headers: { 'content-type': 'text/event-stream' } }); + }); + + await expect(scanProductionCatalog({ + apiKey: 'secret-key', + fetchImpl: fetchImpl as typeof fetch, + })).resolves.toHaveLength(6); + expect(fetchImpl).toHaveBeenCalledOnce(); + }); + + it('fails closed when auth is absent or production rejects it', async () => { + await expect(scanProductionCatalog()).rejects.toThrow(/AICTRL_API_KEY is required/); + await expect(scanProductionCatalog({ + apiKey: 'invalid', + fetchImpl: vi.fn(async () => new Response('unauthorized', { status: 401 })) as typeof fetch, + })).rejects.toThrow(/HTTP 401/); + }); +}); diff --git a/test/public-packages.test.ts b/test/public-packages.test.ts index 680f395..c58f779 100644 --- a/test/public-packages.test.ts +++ b/test/public-packages.test.ts @@ -36,6 +36,24 @@ describe('public vendor packages', () => { }); }); + it('uses one canonical workflow MCP resource for every vendor package', () => { + const lock = json('public-skills.lock.json'); + const codex = json('plugins/aictrl/.codex-plugin/plugin.json'); + const claude = json('claude/aictrl/.claude-plugin/plugin.json'); + const opencode = json('opencode/package.json'); + const manifest = json('opencode/skills-manifest.json'); + + expect(json('plugins/aictrl/.mcp.json').mcpServers.aictrl.url).toBe( + publicMcpUrl(), + ); + expect(json('claude/aictrl/.mcp.json').mcpServers.aictrl.url).toBe( + publicMcpUrl(), + ); + expect(manifest).toEqual({ skillsVersion: lock.skillsVersion, skills: lock.skills }); + expect(opencode.version).toBe(codex.version); + expect(opencode.version).toBe(claude.version); + }); + it('installs, repeats, and uninstalls OpenCode without clobbering unrelated config', () => { const root = mkdtempSync(join(tmpdir(), 'aictrl-opencode-test-')); const configRoot = join(root, 'opencode'); @@ -47,6 +65,7 @@ describe('public vendor packages', () => { ); const env = { ...process.env, XDG_CONFIG_HOME: root }; const installer = join(repoRoot, 'opencode/bin/install.js'); + const expectedMcpUrl = publicMcpUrl(); execFileSync(process.execPath, [installer], { env }); execFileSync(process.execPath, [installer], { env }); @@ -56,7 +75,7 @@ describe('public vendor packages', () => { theme: 'system', mcp: { existing: { type: 'remote', url: 'https://example.com/mcp' }, - aictrl: { type: 'remote', url: 'https://aictrl.dev/mcp/workflows', enabled: true }, + aictrl: { type: 'remote', url: expectedMcpUrl, enabled: true }, }, }); @@ -67,6 +86,53 @@ describe('public vendor packages', () => { }); }); + it('upgrades AICtrl-managed OpenCode state without removing unrelated skills', () => { + const root = mkdtempSync(join(tmpdir(), 'aictrl-opencode-upgrade-')); + const configRoot = join(root, 'opencode'); + const configFile = join(configRoot, 'opencode.json'); + const skillsRoot = join(configRoot, 'skills'); + const managedSkill = join(skillsRoot, 'implement-code-change'); + const unrelatedSkill = join(skillsRoot, 'team-custom'); + mkdirSync(managedSkill, { recursive: true }); + mkdirSync(unrelatedSkill, { recursive: true }); + writeFileSync(join(managedSkill, 'SKILL.md'), 'stale managed skill\n'); + writeFileSync(join(unrelatedSkill, 'SKILL.md'), 'unrelated team skill\n'); + writeFileSync( + configFile, + JSON.stringify({ + theme: 'system', + mcp: { + existing: { type: 'remote', url: 'https://example.com/mcp' }, + aictrl: { + type: 'remote', + url: 'https://aictrl.dev/mcp/workflows/opencode-ecosystem/0.1.0-beta.1/implement-code-change/1.0.0', + enabled: true, + }, + }, + }), + ); + + execFileSync(process.execPath, [join(repoRoot, 'opencode/bin/install.js')], { + env: { ...process.env, XDG_CONFIG_HOME: root }, + }); + + expect(jsonAt(configFile)).toMatchObject({ + theme: 'system', + mcp: { + existing: { type: 'remote', url: 'https://example.com/mcp' }, + aictrl: { + type: 'remote', + url: publicMcpUrl(), + enabled: true, + }, + }, + }); + expect(readFileSync(join(managedSkill, 'SKILL.md'), 'utf8')).toBe( + readFileSync(join(repoRoot, 'opencode/skills/implement-code-change/SKILL.md'), 'utf8'), + ); + expect(readFileSync(join(unrelatedSkill, 'SKILL.md'), 'utf8')).toBe('unrelated team skill\n'); + }); + it('fails closed instead of overwriting malformed OpenCode config', () => { const root = mkdtempSync(join(tmpdir(), 'aictrl-opencode-invalid-')); const configRoot = join(root, 'opencode'); @@ -91,3 +157,7 @@ function json(path: string): any { function jsonAt(path: string): any { return JSON.parse(readFileSync(path, 'utf8')); } + +function publicMcpUrl(): string { + return 'https://aictrl.dev/mcp'; +} diff --git a/test/release-publication.test.ts b/test/release-publication.test.ts new file mode 100644 index 0000000..e7ab7c0 --- /dev/null +++ b/test/release-publication.test.ts @@ -0,0 +1,64 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { vi } from 'vitest'; + +import { inspectReleasePublication } from '../scripts/verify-release-publication.mjs'; + +const packageSpec = './opencode'; +const packageMetadata = JSON.parse( + readFileSync(resolve(import.meta.dirname, '..', 'opencode', 'package.json'), 'utf8'), +) as { name: string; version: string }; +const packageIdentity = { name: packageMetadata.name, version: packageMetadata.version }; + +describe('release publication verifier', () => { + it('allows publication when the exact version is absent', async () => { + const pack = vi.fn(); + const fetchImpl = vi.fn().mockResolvedValue(new Response(null, { status: 404 })); + const result = await inspectReleasePublication(packageSpec, { + fetchImpl, + pack, + registry: 'https://registry.example.test', + }); + + expect(result).toEqual({ published: false, ...packageIdentity }); + expect(fetchImpl).toHaveBeenCalledWith( + `https://registry.example.test/%40aictrl%2Fopencode/${packageIdentity.version}`, + { headers: { accept: 'application/json' } }, + ); + expect(pack).not.toHaveBeenCalled(); + }); + + it('skips publication only when the published and local integrities match', async () => { + const result = await inspectReleasePublication(packageSpec, { + fetchImpl: vi.fn().mockResolvedValue( + Response.json({ dist: { integrity: 'sha512-matching' } }), + ), + pack: vi.fn().mockResolvedValue({ ...packageIdentity, integrity: 'sha512-matching' }), + registry: 'https://registry.example.test/', + }); + + expect(result).toEqual({ published: true, ...packageIdentity }); + }); + + it('rejects an existing version with different package contents', async () => { + await expect( + inspectReleasePublication(packageSpec, { + fetchImpl: vi.fn().mockResolvedValue( + Response.json({ dist: { integrity: 'sha512-published' } }), + ), + pack: vi.fn().mockResolvedValue({ ...packageIdentity, integrity: 'sha512-local' }), + registry: 'https://registry.example.test', + }), + ).rejects.toThrow('does not match the local release package'); + }); + + it('fails closed on registry errors', async () => { + await expect( + inspectReleasePublication(packageSpec, { + fetchImpl: vi.fn().mockResolvedValue(new Response(null, { status: 503 })), + pack: vi.fn(), + registry: 'https://registry.example.test', + }), + ).rejects.toThrow('npm registry returned HTTP 503'); + }); +}); diff --git a/test/release-target.test.ts b/test/release-target.test.ts new file mode 100644 index 0000000..c3d9d65 --- /dev/null +++ b/test/release-target.test.ts @@ -0,0 +1,26 @@ +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const repoRoot = resolve(import.meta.dirname, '..'); +const resolver = resolve(repoRoot, 'scripts/resolve-release-target.mjs'); +const rootVersion = JSON.parse(readFileSync(resolve(repoRoot, 'package.json'), 'utf8')).version; +const publicVersion = JSON.parse(readFileSync(resolve(repoRoot, 'opencode/package.json'), 'utf8')).version; + +describe('release target resolver', () => { + it('routes public beta releases to the OpenCode package', () => { + expect(resolveTag(`public-v${publicVersion}`)).toBe('package_spec=./opencode\ndist_tag=beta\n'); + }); + + it('routes root package releases independently', () => { + expect(resolveTag(`v${rootVersion}`)).toBe('package_spec=.\ndist_tag=latest\n'); + }); + + it.each(['public-v0.0.0', 'v0.0.0', 'release-1'])('rejects an invalid or stale tag: %s', (tag) => { + expect(() => resolveTag(tag)).toThrow(); + }); +}); + +function resolveTag(tag: string): string { + return execFileSync(process.execPath, [resolver, tag], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); +}