feat: drafting CLI, preview deploys, DTPR Claude Code plugin (P3)#266
feat: drafting CLI, preview deploys, DTPR Claude Code plugin (P3)#266
Conversation
- schema:new <type> <date-beta> copies the newest existing version into a new beta directory and rewrites meta.yaml with beta status, fresh created_at, and a sentinel content_hash stamped by schema:build. Source resolution picks the newest date; same date prefers stable. - schema:promote <type>@<date>-beta validates the beta, renames the directory, rewrites meta.yaml as stable, and creates a git branch schema/promote-<type>-<date> with the rename staged and committed — ready for the user to push and open a PR. Covers parent plan R21 + R23. Also lands the plan doc that reorganizes parent Units 13-15 into this drafting-skills plan.
- api-preview-deploy.yaml runs on pull_request (labeled + synchronize) and is gated by the `schema:preview` label so unrelated PRs never trigger a deploy. Mirrors the prod workflow shape but uses the _PREVIEW token + R2 bucket and deploys via `wrangler --env preview`. - noindex middleware registers unconditionally on every route; at request time it checks `c.env.ENVIRONMENT === 'preview'` and adds `X-Robots-Tag: noindex, nofollow` only then. Prod never sets the var, so prod responses are untouched. - wrangler.jsonc env.preview now carries `vars.ENVIRONMENT: "preview"`. worker-configuration.d.ts is regenerated so the new binding typechecks. - api/docs/preview-deployments.md documents the label, required secrets, and verification workflow.
… (Unit 3) - plugin/dtpr/.claude-plugin/plugin.json: plugin manifest. - plugin/dtpr/.mcp.json: registers remote HTTP MCP at api.dtpr.io/mcp with a static User-Agent header so the Worker can attribute plugin traffic separately from generic MCP clients. - plugin/dtpr/README.md: install, skills summary, troubleshoot. - .claude-plugin/marketplace.json: top-level marketplace listing with plugin/dtpr/ as a local-path plugin. Users install with `/plugin marketplace add Helpful-Places/dtpr` + `/plugin install dtpr`. - root README.md: short "Claude Code plugin" section pointing at the new plugin directory. Skills and eval harness (Units 4 + 5) land in follow-up commits.
- skills/dtpr-describe-system/SKILL.md: 5-phase workflow for turning a natural-language AI-system description into a validated DTPR datachain. References the 7 MCP tools by name in workflow prose; the MCP server owns tool-level documentation via Zod .describe() text. - evals/describe-system.evals.json: 5 should-trigger prompts (varied domains: parking, library, 311, HR, gunshot detector) + 5 should-not-trigger (including one should-trigger prompt for the sibling brainstorm skill, so the two skills don't collide). - evals/verify.mjs: offline conformance check that validates SKILL.md frontmatter, eval JSON shape, and cross-references backticked tool names in the skill body against api/src/mcp/tools.ts — catches drift when MCP tools are renamed upstream. - root test:plugin script + .github/workflows/plugin-test.yaml so the conformance check runs on every PR that touches plugin/, the marketplace manifest, or the MCP tool registry.
- skills/dtpr-schema-brainstorm/SKILL.md: 4-phase workflow for gap-analyzing the DTPR taxonomy against a novel scenario and producing a concrete schema-edit proposal. Does not invoke the CLI or modify api/schemas/ — the proposal ends with a `schema:new` command line for the user to run. - evals/schema-brainstorm.evals.json: 5 should-trigger prompts (LLM hallucination, generative output, accountable deep-dive, third-party processor, retiring cloud_storage) + 5 should-not-trigger including a describe-system prompt so routing between the sibling skills is exercised both ways. - verify.mjs (Unit 4) already iterates all skills and eval sets, so no extension needed here.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
dtpr-docs | 56644a0 | Apr 17 2026, 01:14 PM |
Greptile SummaryShips five units on top of the existing read-side REST + MCP: a
Confidence Score: 4/5Safe to merge after addressing the promote rollback gap; P2s are non-blocking. One P1 correctness issue in api/cli/commands/promote.ts — missing branch-existence pre-check before the point of no return (rename).
|
| Filename | Overview |
|---|---|
| api/cli/commands/promote.ts | Schema promote command — renames beta to stable, rewrites meta.yaml, creates git branch/commit; missing pre-rename branch-existence check means a git checkout -b failure after the rename leaves the filesystem in an inconsistent state with no rollback. |
| api/cli/commands/new.ts | Schema draft command — copies newest version into beta dir, rewrites meta.yaml with sentinel hash; clean pure-function design with correct overwrite guard. |
| api/src/middleware/noindex.ts | Post-response middleware that stamps X-Robots-Tag: noindex, nofollow only when ENVIRONMENT === "preview"; correct Hono pattern, well-tested. |
| .github/workflows/api-preview-deploy.yaml | Label-gated preview deploy workflow with typecheck, test, schema build, and smoke-test steps; SCHEMA_VERSION is hardcoded and will break when the referenced beta is promoted. |
| plugin/dtpr/evals/verify.mjs | Offline conformance checker for skill frontmatter, eval JSON shape, and MCP tool name drift; doesn't verify that data.skill in each eval set references an existing skill directory. |
| api/src/app.ts | Hono app wiring — adds noindex() middleware to the stack; correct position and order relative to CORS, request-id, and logging. |
| .github/workflows/plugin-test.yaml | Plugin conformance CI — runs npm run test:plugin (pure Node builtins, no install needed); correct path triggers covering plugin/, .claude-plugin/, and api/src/mcp/tools.ts. |
| plugin/dtpr/skills/dtpr-describe-system/SKILL.md | 5-phase workflow skill for building schema-validated DTPR datachains; well-structured with clear phase tables, tool reference, and retry-cap for validation loops. |
| plugin/dtpr/skills/dtpr-schema-brainstorm/SKILL.md | 4-phase taxonomy-brainstorming skill with explicit non-goal of modifying files; correct skill boundary with sibling routing guidance. |
| api/cli/bin.ts | CLI entry point adding new and promote subcommands with usage-error exit codes (2) consistent with existing build/validate pattern. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[schema:promote type@date-beta] --> B{stable dir exists?}
B -- yes --> FAIL1[return ok:false\nstable is immutable]
B -- no --> C[validateCmd]
C -- fail --> FAIL2[return ok:false\nvalidation failed]
C -- ok --> D{skipGit?}
D -- yes --> RENAME
D -- no --> E[git rev-parse\nis-inside-work-tree]
E -- no --> FAIL3[return ok:false\nnot a git repo]
E -- yes --> F[git status --porcelain]
F -- dirty --> FAIL4[return ok:false\nworking tree dirty]
F -- clean --> RENAME
RENAME[rename betaDir to stableDir]
RENAME --> WRITE[writeFile meta.yaml status: stable]
WRITE --> G{skipGit?}
G -- yes --> OK
G -- no --> H[git checkout -b branch]
H -- fail --> ERR[throws — no rollback]
H -- ok --> I[git add -A]
I --> J[git commit]
J --> OK[return ok:true]
Prompt To Fix All With AI
This is a comment left during a code review.
Path: api/cli/commands/promote.ts
Line: 110-139
Comment:
**No rollback if `git checkout -b` fails after the rename**
`rename(betaDir, stableDir)` and `writeFile(metaPath, ...)` are applied at lines 111–127, but `git checkout -b branch` (line 132) can fail with exit 128 ("branch already exists") if the named branch was created by a prior partial run. When that happens, `gitRun` rejects, the error propagates uncaught, and the caller gets a thrown exception — not a `PromoteResult` — while the filesystem is already in the renamed stable state with no beta to go back to.
The JSDoc contract says "Fails (without mutating anything) if…" but that guarantee only covers the pre-rename checks. A re-entry scenario — user deleted the stable dir to retry, branch still present locally — will corrupt the working tree.
Consider checking for an existing branch **before** the rename, or adding a `try/finally` that calls `rename(stableDir, betaDir)` on failure:
```ts
// Before rename: pre-check the branch name
if (!options.skipGit) {
const branchExists = await gitOk(['rev-parse', '--verify', `refs/heads/${branch}`], gitRoot)
if (branchExists) {
err(`error: branch '${branch}' already exists. Delete it with 'git branch -D ${branch}' before re-promoting.`)
return { ok: false, betaVersion: beta.canonical }
}
}
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: .github/workflows/api-preview-deploy.yaml
Line: 18-21
Comment:
**Hardcoded `SCHEMA_VERSION` will go stale**
`SCHEMA_VERSION: ai@2026-04-16-beta` is baked into the workflow. Once this beta is promoted to stable (the directory is deleted and replaced by `2026-04-16/`), the `schema:build` step will fail on every labeled PR because the version no longer exists in `api/schemas/`. Consider deriving it dynamically or accepting it as a `workflow_dispatch` input so it can be overridden without touching the workflow file.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: plugin/dtpr/evals/verify.mjs
Line: 86-117
Comment:
**`verifyEvalSet` doesn't cross-reference `data.skill` against actual skill directories**
Every eval file has a `"skill"` field (e.g. `"skill": "dtpr-describe-system"`), but `verifyEvalSet` only validates the JSON structure — it never checks that `data.skill` matches a directory under `SKILLS_DIR`. Renaming or deleting a skill would orphan its eval set silently; `verify.mjs` would still report success.
A one-liner after the `JSON.parse` would close the gap:
```js
if (typeof data.skill === 'string' && !skillDirNames.has(data.skill)) {
fail(`${evalPath}: 'skill' field '${data.skill}' does not match any skill directory in ${SKILLS_DIR}.`)
}
```
where `skillDirNames` is the set of directory names collected in `main()` before `verifyEvalSet` is called.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "feat(plugin): dtpr-schema-brainstorm ski..." | Re-trigger Greptile
| // Rename the dir on disk. | ||
| await rename(betaDir, stableDir) | ||
|
|
||
| // Rewrite meta.yaml in the new stable dir. | ||
| const metaPath = join(stableDir, 'meta.yaml') | ||
| const existingRaw = await readFile(metaPath, 'utf8') | ||
| const existing = yaml.load(existingRaw, { schema: yaml.JSON_SCHEMA }) as Record<string, unknown> | ||
|
|
||
| const manifest = { | ||
| version: stableCanonical, | ||
| status: 'stable' as const, | ||
| created_at: existing.created_at, | ||
| notes: `Promoted from ${beta.canonical}`, | ||
| content_hash: existing.content_hash, | ||
| locales: existing.locales, | ||
| } | ||
| SchemaManifestSchema.parse(manifest) | ||
| await writeFile(metaPath, toYaml(manifest), 'utf8') | ||
|
|
||
| const branch = `schema/promote-${beta.type}-${beta.date}` | ||
| if (!options.skipGit) { | ||
| // Create the branch, stage the rename, commit. | ||
| await gitRun(['checkout', '-b', branch], gitRoot) | ||
| // `git add -A <path>` picks up both the new tree and the removed beta. | ||
| await gitRun(['add', '-A', join('api', 'schemas', beta.type)], gitRoot) | ||
| await gitRun( | ||
| ['commit', '-m', `promote schema ${stableCanonical}`], | ||
| gitRoot, | ||
| ) | ||
| } |
There was a problem hiding this comment.
No rollback if
git checkout -b fails after the rename
rename(betaDir, stableDir) and writeFile(metaPath, ...) are applied at lines 111–127, but git checkout -b branch (line 132) can fail with exit 128 ("branch already exists") if the named branch was created by a prior partial run. When that happens, gitRun rejects, the error propagates uncaught, and the caller gets a thrown exception — not a PromoteResult — while the filesystem is already in the renamed stable state with no beta to go back to.
The JSDoc contract says "Fails (without mutating anything) if…" but that guarantee only covers the pre-rename checks. A re-entry scenario — user deleted the stable dir to retry, branch still present locally — will corrupt the working tree.
Consider checking for an existing branch before the rename, or adding a try/finally that calls rename(stableDir, betaDir) on failure:
// Before rename: pre-check the branch name
if (!options.skipGit) {
const branchExists = await gitOk(['rev-parse', '--verify', `refs/heads/${branch}`], gitRoot)
if (branchExists) {
err(`error: branch '${branch}' already exists. Delete it with 'git branch -D ${branch}' before re-promoting.`)
return { ok: false, betaVersion: beta.canonical }
}
}Prompt To Fix With AI
This is a comment left during a code review.
Path: api/cli/commands/promote.ts
Line: 110-139
Comment:
**No rollback if `git checkout -b` fails after the rename**
`rename(betaDir, stableDir)` and `writeFile(metaPath, ...)` are applied at lines 111–127, but `git checkout -b branch` (line 132) can fail with exit 128 ("branch already exists") if the named branch was created by a prior partial run. When that happens, `gitRun` rejects, the error propagates uncaught, and the caller gets a thrown exception — not a `PromoteResult` — while the filesystem is already in the renamed stable state with no beta to go back to.
The JSDoc contract says "Fails (without mutating anything) if…" but that guarantee only covers the pre-rename checks. A re-entry scenario — user deleted the stable dir to retry, branch still present locally — will corrupt the working tree.
Consider checking for an existing branch **before** the rename, or adding a `try/finally` that calls `rename(stableDir, betaDir)` on failure:
```ts
// Before rename: pre-check the branch name
if (!options.skipGit) {
const branchExists = await gitOk(['rev-parse', '--verify', `refs/heads/${branch}`], gitRoot)
if (branchExists) {
err(`error: branch '${branch}' already exists. Delete it with 'git branch -D ${branch}' before re-promoting.`)
return { ok: false, betaVersion: beta.canonical }
}
}
```
How can I resolve this? If you propose a fix, please make it concise.| env: | ||
| SCHEMA_VERSION: ai@2026-04-16-beta | ||
| steps: | ||
| - uses: actions/checkout@v4 |
There was a problem hiding this comment.
Hardcoded
SCHEMA_VERSION will go stale
SCHEMA_VERSION: ai@2026-04-16-beta is baked into the workflow. Once this beta is promoted to stable (the directory is deleted and replaced by 2026-04-16/), the schema:build step will fail on every labeled PR because the version no longer exists in api/schemas/. Consider deriving it dynamically or accepting it as a workflow_dispatch input so it can be overridden without touching the workflow file.
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/api-preview-deploy.yaml
Line: 18-21
Comment:
**Hardcoded `SCHEMA_VERSION` will go stale**
`SCHEMA_VERSION: ai@2026-04-16-beta` is baked into the workflow. Once this beta is promoted to stable (the directory is deleted and replaced by `2026-04-16/`), the `schema:build` step will fail on every labeled PR because the version no longer exists in `api/schemas/`. Consider deriving it dynamically or accepting it as a `workflow_dispatch` input so it can be overridden without touching the workflow file.
How can I resolve this? If you propose a fix, please make it concise.| function verifyEvalSet(evalFile) { | ||
| const evalPath = join(EVALS_DIR, evalFile) | ||
| let data | ||
| try { | ||
| data = JSON.parse(readFileSync(evalPath, 'utf8')) | ||
| } catch (e) { | ||
| fail(`${evalPath}: invalid JSON (${e.message}).`) | ||
| return | ||
| } | ||
| for (const key of ['should_trigger', 'should_not_trigger']) { | ||
| const arr = data[key] | ||
| if (!Array.isArray(arr) || arr.length === 0) { | ||
| fail(`${evalPath}: '${key}' must be a non-empty array.`) | ||
| continue | ||
| } | ||
| const ids = new Set() | ||
| for (const entry of arr) { | ||
| if (typeof entry !== 'object' || entry === null) { | ||
| fail(`${evalPath}: '${key}' contains a non-object entry.`) | ||
| continue | ||
| } | ||
| if (!entry.id || ids.has(entry.id)) { | ||
| fail(`${evalPath}: '${key}' entry is missing 'id' or has a duplicate id.`) | ||
| } else { | ||
| ids.add(entry.id) | ||
| } | ||
| if (typeof entry.prompt !== 'string' || entry.prompt.length === 0) { | ||
| fail(`${evalPath}: '${key}' entry '${entry.id ?? '?'}' is missing a non-empty 'prompt'.`) | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
verifyEvalSet doesn't cross-reference data.skill against actual skill directories
Every eval file has a "skill" field (e.g. "skill": "dtpr-describe-system"), but verifyEvalSet only validates the JSON structure — it never checks that data.skill matches a directory under SKILLS_DIR. Renaming or deleting a skill would orphan its eval set silently; verify.mjs would still report success.
A one-liner after the JSON.parse would close the gap:
if (typeof data.skill === 'string' && !skillDirNames.has(data.skill)) {
fail(`${evalPath}: 'skill' field '${data.skill}' does not match any skill directory in ${SKILLS_DIR}.`)
}where skillDirNames is the set of directory names collected in main() before verifyEvalSet is called.
Prompt To Fix With AI
This is a comment left during a code review.
Path: plugin/dtpr/evals/verify.mjs
Line: 86-117
Comment:
**`verifyEvalSet` doesn't cross-reference `data.skill` against actual skill directories**
Every eval file has a `"skill"` field (e.g. `"skill": "dtpr-describe-system"`), but `verifyEvalSet` only validates the JSON structure — it never checks that `data.skill` matches a directory under `SKILLS_DIR`. Renaming or deleting a skill would orphan its eval set silently; `verify.mjs` would still report success.
A one-liner after the `JSON.parse` would close the gap:
```js
if (typeof data.skill === 'string' && !skillDirNames.has(data.skill)) {
fail(`${evalPath}: 'skill' field '${data.skill}' does not match any skill directory in ${SKILLS_DIR}.`)
}
```
where `skillDirNames` is the set of directory names collected in `main()` before `verifyEvalSet` is called.
How can I resolve this? If you propose a fix, please make it concise.- promote.ts: pre-check that the target branch does not already exist *before* the rename. `git checkout -b` fails with exit 128 when the branch is a leftover from a prior partial run, which would otherwise leave the tree half-promoted (directory renamed but no commit). Rollback guarantee in the JSDoc now holds for every failure path. New test covers the case. - api-preview-deploy.yaml: derive SCHEMA_VERSION from the newest *-beta directory under api/schemas/ at run time instead of hardcoding it. Hardcoded version would go stale the moment that beta gets promoted to stable. - verify.mjs: cross-reference each eval file's `skill` field against the set of skill directory names. Orphaned eval sets (e.g. after a skill rename) now fail the conformance check instead of passing silently.
Summary
Closes Phase P3 of the parent DTPR API plan (
docs/plans/2026-04-16-001-feat-dtpr-api-mcp-plan.md). Ships five units on top of the read-side REST + MCP that landed in #262 / #265.pnpm schema:new <type> <YYYY-MM-DD-beta>copies the newest existing version into a new beta directory and rewritesmeta.yamlwith a sentinelcontent_hashthatschema:buildstamps on emit.pnpm schema:promote <type>@<date>-betavalidates the beta, renames the directory to its stable form, rewrites the manifest, and creates aschema/promote-<type>-<date>branch with the rename committed — ready for the user to push and PR. Both are pure-function-plus-thin-fs-wrapper and tested against a 2-cat / 2-el tmpdir fixture (11 new tests)..github/workflows/api-preview-deploy.yamlis label-gated onschema:preview, mirrors the prod workflow shape, and deploys viawrangler --env preview+ an atomic R2 upload todtpr-api-preview. A newnoindexHono middleware readsc.env.ENVIRONMENTat request time and stampsX-Robots-Tag: noindex, nofollowonly when the preview env sets the var, so production responses are untouched. Preview deploy dry-runs cleanly; 203/203 worker tests stay green.plugin/dtpr/ships a.claude-plugin/plugin.json, a.mcp.jsonthat registersapi.dtpr.io/mcpas a remote HTTP MCP, and aREADME.md..claude-plugin/marketplace.jsonat repo root lets users install via/plugin marketplace add Helpful-Places/dtpr+/plugin install dtpr.dtpr-describe-systemturns a natural-language AI-system description into a schema-validated DTPR datachain, driving the 7 MCP tools through a 5-phase workflow.dtpr-schema-brainstormstress-tests the current taxonomy against a novel scenario and emits a concrete schema-edit proposal ending with theschema:newcommand line for the user to run (the skill never shells out or touchesapi/schemas/). Skill bodies reference MCP tool names in prose only; the MCP server owns the tool descriptions.plugin/dtpr/evals/verify.mjsis an offline conformance check invoked bypnpm test:pluginand the new.github/workflows/plugin-test.yaml. It validates frontmatter on each SKILL.md, eval JSON shape, and cross-references backticked tool-name tokens againstapi/src/mcp/tools.tsso skill bodies can't silently drift when MCP tools are renamed upstream. Each skill ships 5 should-trigger + 5 should-not-trigger prompts, with sibling-skill prompts used as negatives so routing between the two skills is exercised both ways.Test plan
pnpm --filter ./api typecheckcleanpnpm --filter ./api test:cli— 26/26 pass (CLI + migration)pnpm --filter ./api test:workers— 203/203 pass (REST + MCP + middleware, incl. new noindex tests)node plugin/dtpr/evals/verify.mjs— 2 skills, 2 eval sets, 7 MCP tools passwrangler deploy --env preview --dry-runsucceeds with the newENVIRONMENTbindingschema:previewlabel to a throwaway PR → workflow runs →curl -sI https://api-preview.dtpr.io/healthzreturnsX-Robots-Tag: noindex, nofollow/plugin marketplace add Helpful-Places/dtpr+/plugin install dtpr→/mcplistsdtpr→describe our facial-recognition parking kiosk as a DTPR datachainproduces avalidate_datachain: ok:trueresponsePlan:
docs/plans/2026-04-17-001-feat-dtpr-drafting-skills-plan.md.