chore(voyage): release v5.0.1 — drop standalone HTML render; print literal /playground document-critique invocation
The v5.0.0 stop-gap had /trekbrief, /trekplan, and /trekreview each render
a read-only {artifact}.html (via scripts/render-artifact.mjs) AND print a
vague "run the /playground plugin" instruction. In practice the read-only
HTML was redundant with what /playground produces and the instruction
wasn't copy-paste-ready — the operator had to guess the right invocation.
v5.0.1 deletes scripts/render-artifact.mjs + its test + npm run render,
and makes each producing command end with a single boxed, literal,
copy-paste-ready line:
/playground build a document-critique playground for {artifact_path}
One paste from the operator launches the official playground skill's
document-critique template, which builds an interactive HTML — artifact
on the left, per-line Approve/Reject/Comment cards on the right, Copy
Prompt button at the bottom. Mark suggestions, click Copy Prompt, paste
back, Claude revises the .md. Doc-consistency test pins the literal
invocation so the prose cannot soften back into vagueness.
npm test green: 503 tests, 501 pass, 0 fail, 2 skipped.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
916d30f63e
commit
2e0892cdaf
15 changed files with 206 additions and 563 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "voyage",
|
||||
"description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline; renders produced artifacts to HTML + link, annotate via the /playground plugin.",
|
||||
"version": "5.0.0",
|
||||
"description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline. At the end of /trekbrief, /trekplan, and /trekreview, the operator gets a copy-paste-ready /playground invocation that builds an interactive document-critique HTML for the artifact.",
|
||||
"version": "5.0.1",
|
||||
"author": {
|
||||
"name": "Kjell Tore Guttormsen"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,6 +4,52 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## v5.0.1 — 2026-05-13 — Drop the standalone HTML render; print a literal /playground invocation
|
||||
|
||||
**No new breaking changes beyond v5.0.0.** Forks that consumed
|
||||
`scripts/render-artifact.mjs` directly (or invoked `npm run render`) must
|
||||
remove that integration. Nothing else moves.
|
||||
|
||||
### Why
|
||||
|
||||
v5.0.0 had `/trekbrief`, `/trekplan`, and `/trekreview` each finish by
|
||||
*both* rendering a read-only `{artifact}.html` view (via the new
|
||||
`scripts/render-artifact.mjs`) *and* printing a vague instruction to "run
|
||||
the `/playground` plugin (`document-critique` template) on the `.md` and
|
||||
paste the prompt back". In practice the operator saw two HTMLs in their
|
||||
project dir, no annotation UI on the rendered `.html`, and had to guess
|
||||
the right `/playground` invocation. The read-only `.html` added confusion
|
||||
without affording annotation — it duplicated work the `/playground`
|
||||
HTML already does (formatted document on the left, annotations on the
|
||||
right, Copy Prompt button at the bottom).
|
||||
|
||||
v5.0.1 deletes the redundant render and makes the printed `/playground`
|
||||
invocation literal and copy-paste-ready. One paste from the operator
|
||||
launches the `playground` skill, which loads its `document-critique`
|
||||
template, reads the `.md`, builds the interactive HTML, opens it. Mark
|
||||
suggestions, click Copy Prompt, paste back. Done.
|
||||
|
||||
### Removed
|
||||
|
||||
- **`scripts/render-artifact.mjs`** — the v5.0.0 standalone Markdown→HTML renderer (~280 lines, zero deps). Redundant with `/playground`'s HTML.
|
||||
- **`tests/scripts/render-artifact.test.mjs`** (and the now-empty `tests/scripts/` dir).
|
||||
- **`npm run render`** script alias in `package.json`.
|
||||
- All references to `render-artifact.mjs`, `brief.html`, `plan.html`, `review.html` in `CLAUDE.md` (plugin + root), `README.md` (plugin + root), `.claude-plugin/marketplace.json`, and the three command files' final-output blocks.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`commands/trekbrief.md` Step 4g (Finalize), `commands/trekplan.md` Phase 10 (Present and refine), `commands/trekreview.md` Phase 9 (Present summary)** — each now ends by printing a single boxed block with the literal text `/playground build a document-critique playground for {abs_path}` and a one-paragraph explanation of the paste-mark-copy-paste loop. The literal string is pinned by `tests/lib/doc-consistency.test.mjs` so it cannot soften back into "run the `/playground` plugin" without a test failure.
|
||||
- **`tests/lib/doc-consistency.test.mjs`** — replaced the v5.0.0 `render-artifact.mjs exists` + `producing commands reference render-artifact.mjs` pins with v5.0.1 pins: `render-artifact.mjs` *no longer* exists; producing commands include the literal `/playground build a document-critique playground for` invocation; producing commands no longer reference `render-artifact.mjs`; `package.json scripts.render` is gone; CHANGELOG has both v5.0.0 and v5.0.1 entries.
|
||||
- **Plugin `CLAUDE.md`** — "Render-and-link (v5.0.0)" paragraph rewritten to "Post-command annotation invocation (v5.0.1)" explaining the literal-paste contract; project-directory contract no longer lists `.html` siblings; "State" section's project-root inventory no longer lists `.html` files.
|
||||
- **Plugin `README.md`** — "Rendered artifacts & annotation (v5.0.0)" section rewritten to "Reviewing and annotating artifacts (v5.0.1)" with a worked example of the printed output and a "What v5.0.1 changed from v5.0.0" sub-note. Top-of-README one-liner + bottom "Known limitations" note updated.
|
||||
- **Root `CLAUDE.md`** + **root `README.md`** + **`.claude-plugin/marketplace.json`** — voyage description updated to v5.0.1 + the one-paste invocation model.
|
||||
|
||||
### Notes
|
||||
|
||||
- `/playground` is the `playground` skill from `claude-plugins-official`. It must be installed in the operator's environment for the printed command to work. If it isn't, the same effect is achievable by pasting the `.md` content into Claude with "review this and suggest changes" — manual freehand revision.
|
||||
- `npm test`: 503 tests, 501 pass, 0 fail, 2 skipped (down from 509 — 8 `render-artifact.test.mjs` tests removed; the doc-consistency pins were updated to v5.0.1 contracts, net +2 tests).
|
||||
- Version bump 5.0.0 → 5.0.1 in `.claude-plugin/plugin.json`, `package.json`, `package-lock.json`, plugin `README.md` badge.
|
||||
|
||||
## v5.0.0 — 2026-05-12 — Remove the bespoke playground; render artifacts to HTML + link
|
||||
|
||||
**Breaking.** `/trekrevise` is removed. The `playground/` directory, Handover 8
|
||||
|
|
|
|||
|
|
@ -232,42 +232,41 @@ Local Docker Compose stack: `examples/observability/`. Operator docs: `docs/obse
|
|||
|
||||
**Continue:** `/trekcontinue` reads `{dir}/.session-state.local.json` (Handover 7), validates schema-v1 via `session-state-validator`, narrates a 3-line summary (project / next-session-label / brief-path), and immediately begins executing the next session. Auto-discovers active project state files under `.claude/projects/*/.session-state.local.json` if no explicit `<project-dir>` argument. Operator-invoked only — never auto-loaded via SessionStart. The `/trekendsession` helper is the informal-flow producer: writes the same state file for ad-hoc multi-session handovers that don't run through `/trekexecute`.
|
||||
|
||||
**Render-and-link (v5.0.0):** the last step of `/trekbrief`, `/trekplan`, and `/trekreview` renders the just-written `.md` artifact to a self-contained `.html` in the same project directory (`scripts/render-artifact.mjs` — zero npm deps, zero external network, design-system-styled, frontmatter folded into a `<details>` block) and prints the `file://` link. To annotate, the operator runs the official `/playground` plugin (`document-critique` template) on the `.md` and pastes the generated prompt back into the conversation; Claude revises the artifact freehand. This replaces the v4.2/v4.3 bespoke playground SPA + `/trekrevise` + Handover 8 (annotation → revision), all removed in v5.0.0 — see [CHANGELOG.md](CHANGELOG.md) § v5.0.0 for why (the bespoke playground duplicated capabilities the official `/playground` plugin already provides).
|
||||
**Post-command annotation invocation (v5.0.1):** the last step of `/trekbrief`, `/trekplan`, and `/trekreview` prints — verbatim — a copy-paste-ready `/playground` command pointing at the just-written artifact. Concretely: `/playground build a document-critique playground for {abs_path}/{artifact}.md`. When the operator pastes that into Claude, the official `claude-plugins-official` `playground` skill loads its `document-critique` template, reads the `.md`, generates per-line suggestions, and writes a single self-contained HTML file with the artifact nicely formatted on the left, Approve/Reject/Comment cards on the right, and a "Copy Prompt" button at the bottom. The operator marks suggestions, clicks Copy Prompt, pastes the prompt back into Claude — Claude revises the artifact freehand from the notes. **One paste → playground → mark → copy → paste back.** The v5.0.0 standalone `.html` render (`scripts/render-artifact.mjs`) was a separate read-only view that did not afford annotation; v5.0.1 dropped it because it was redundant with the `/playground` HTML (which already shows the artifact nicely) and the two HTMLs created operator confusion. The v4.2/v4.3 bespoke playground SPA + `/trekrevise` + Handover 8 — all removed in v5.0.0 — were never re-introduced. See [CHANGELOG.md](CHANGELOG.md) § v5.0.1.
|
||||
|
||||
**Security:** 4-layer defense-in-depth: plugin hooks (pre-bash-executor, pre-write-executor), prompt-level denylist (works in headless sessions), pre-execution plan scan (Phase 2.4), scoped `--allowedTools` replacing `--dangerously-skip-permissions`. Hard Rules 14-16 enforce verify command security, repo-boundary writes, and sensitive path protection.
|
||||
|
||||
**Pipeline:** `/trekbrief` produces the task brief. `/trekresearch --project <dir>` fills in `{dir}/research/`. `/trekplan --project <dir>` reads brief + research to produce `{dir}/plan.md` (and auto-discovers `{dir}/architecture/overview.md` if an opt-in upstream architect plugin produced one). `/trekexecute --project <dir>` executes and writes `{dir}/progress.json`. `/trekreview --project <dir>` produces `{dir}/review.md`. `/trekbrief`, `/trekplan`, and `/trekreview` each render their artifact to `{dir}/{artifact}.html` and print the link (annotate via the `/playground` plugin). All artifacts live in one project directory.
|
||||
**Pipeline:** `/trekbrief` produces the task brief. `/trekresearch --project <dir>` fills in `{dir}/research/`. `/trekplan --project <dir>` reads brief + research to produce `{dir}/plan.md` (and auto-discovers `{dir}/architecture/overview.md` if an opt-in upstream architect plugin produced one). `/trekexecute --project <dir>` executes and writes `{dir}/progress.json`. `/trekreview --project <dir>` produces `{dir}/review.md`. `/trekbrief`, `/trekplan`, and `/trekreview` each end by printing a copy-paste-ready `/playground build a document-critique playground for {artifact_path}` command — the operator pastes it into Claude to launch the official `playground` skill (`document-critique` template), which builds an interactive HTML for browser annotation. All `.md` artifacts live in one project directory.
|
||||
|
||||
**Project-directory contract (v3.0.0):** trekplan owns the directory layout below. The `architecture/` subdirectory is opt-in and produced by an opt-in upstream architect plugin (not bundled) — the architect plugin is no longer publicly distributed, but the `architecture/overview.md` slot remains available for any compatible producer.
|
||||
|
||||
```
|
||||
.claude/projects/{YYYY-MM-DD}-{slug}/
|
||||
brief.md ← trekbrief writes; everyone reads
|
||||
brief.html ← trekbrief renders (self-contained; for browser viewing / /playground)
|
||||
research/*.md ← trekresearch writes; plan + architect read
|
||||
architecture/ ← OPT-IN, owned by an opt-in upstream architect plugin (not bundled)
|
||||
overview.md
|
||||
gaps.md
|
||||
plan.md ← trekplan writes; trekexecute reads
|
||||
plan.html ← trekplan renders
|
||||
progress.json ← trekexecute writes
|
||||
review.md ← trekreview writes; trekplan reads (Handover 6)
|
||||
review.html ← trekreview renders
|
||||
```
|
||||
|
||||
When an operator runs the `/playground build a document-critique playground for <artifact>` command that the producing commands print, the `playground` skill writes the resulting HTML into its own working directory (typically next to the project dir or in `/tmp/`); voyage does not own the `.html` filename. The annotation HTML is ephemeral — re-built on each invocation from the current `.md` source.
|
||||
|
||||
No code-level dependency between plugins — the contract is filesystem-level only.
|
||||
|
||||
## State
|
||||
|
||||
All artifacts in one project directory (default):
|
||||
- Project root: `.claude/projects/{YYYY-MM-DD}-{slug}/`
|
||||
- `brief.md` + `brief.html` (task brief from `/trekbrief`; `.html` is the self-contained rendered view)
|
||||
- `brief.md` (task brief from `/trekbrief`)
|
||||
- `research/{NN}-{slug}.md` (research briefs from `/trekresearch --project`)
|
||||
- `architecture/overview.md` + `architecture/gaps.md` (opt-in, produced by an opt-in upstream architect plugin, not bundled)
|
||||
- `plan.md` + `plan.html` (from `/trekplan --project`)
|
||||
- `plan.md` (from `/trekplan --project`)
|
||||
- `sessions/session-*.md` (from `--decompose`)
|
||||
- `progress.json` (from `/trekexecute --project`)
|
||||
- `review.md` + `review.html` (from `/trekreview --project`)
|
||||
- `review.md` (from `/trekreview --project`)
|
||||
- `.session-state.local.json` (Handover 7 — gitignored via `*.local.json`; written by `/trekexecute` Phase 8/2.55/4 or `/trekendsession`; read by `/trekcontinue`)
|
||||
|
||||
Legacy paths (still work without `--project`):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# trekplan — Brief, Research, Plan, Execute, Review, Continue
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
|
|
@ -19,9 +19,9 @@ A [Claude Code](https://docs.anthropic.com/en/docs/claude-code) plugin for deep
|
|||
| **`/trekreview`** | Review — independent post-hoc review of delivered code against the brief, severity-tagged findings |
|
||||
| **`/trekcontinue`** | Continue — read `.session-state.local.json` and resume the next session in a multi-session project |
|
||||
|
||||
`/trekbrief`, `/trekplan`, and `/trekreview` also render their artifact to a self-contained `.html` next to it and print the `file://` link — annotate via the official `/playground` plugin (`document-critique`) and paste its prompt back.
|
||||
`/trekbrief`, `/trekplan`, and `/trekreview` each end by printing a single copy-paste-ready `/playground build a document-critique playground for <artifact_path>` command. Paste it into Claude, the official `playground` skill builds a browser-readable HTML with per-line Approve/Reject/Comment annotations, copy the generated prompt, paste back — Claude revises the artifact. See [Reviewing and annotating artifacts](#reviewing-and-annotating-artifacts-v501).
|
||||
|
||||
Every artifact lives in one project directory: `.claude/projects/{YYYY-MM-DD}-{slug}/` contains `brief.md` (+ `brief.html`), `research/NN-*.md`, `plan.md` (+ `plan.html`), `sessions/`, `progress.json`, and `review.md` (+ `review.html`).
|
||||
Every artifact lives in one project directory: `.claude/projects/{YYYY-MM-DD}-{slug}/` contains `brief.md`, `research/NN-*.md`, `plan.md`, `sessions/`, `progress.json`, and `review.md`.
|
||||
|
||||
### Division of labor
|
||||
|
||||
|
|
@ -503,37 +503,55 @@ Both arguments are required. No interactive prompt — headless-safe.
|
|||
|
||||
---
|
||||
|
||||
## Rendered artifacts & annotation (v5.0.0)
|
||||
## Reviewing and annotating artifacts (v5.0.1)
|
||||
|
||||
`/trekbrief`, `/trekplan`, and `/trekreview` each finish by rendering their
|
||||
just-written `.md` to a self-contained `.html` next to it
|
||||
(`{project_dir}/brief.html`, `plan.html`, `review.html`) and printing the
|
||||
`file://` link. The renderer (`scripts/render-artifact.mjs`) is a small,
|
||||
zero-dependency Node script: it folds frontmatter into a `<details>` block,
|
||||
puts code fences in styled `<pre>`, renders tables/lists/links, and inlines a
|
||||
compact design-system-aligned stylesheet. **No external network, no build
|
||||
step, no telemetry.** Two runs on the same input produce byte-identical HTML.
|
||||
`/trekbrief`, `/trekplan`, and `/trekreview` each end by printing a single
|
||||
copy-paste-ready command. After they finish, you see something like:
|
||||
|
||||
To **annotate** an artifact, run the official `/playground` plugin
|
||||
(`document-critique` template) on the `.md` file and paste the prompt it
|
||||
generates back into the conversation — Claude then revises the artifact
|
||||
freehand from your notes. The `/playground` plugin already produces clean,
|
||||
self-contained single-file HTML for exactly this; voyage no longer ships its
|
||||
own annotation UI.
|
||||
```
|
||||
────────────────────────────────────────────────────────────────────
|
||||
To review and annotate this brief, copy and paste this into Claude:
|
||||
|
||||
```bash
|
||||
# Render any artifact manually (the producing commands do this automatically):
|
||||
node plugins/voyage/scripts/render-artifact.mjs \
|
||||
.claude/projects/2026-05-09-feature/plan.md
|
||||
# → writes .claude/projects/2026-05-09-feature/plan.html, prints the path
|
||||
/playground build a document-critique playground for .claude/projects/2026-05-13-foo/brief.md
|
||||
|
||||
That builds a self-contained HTML file with the brief on the left,
|
||||
per-line approve/reject/comment annotations on the right, and a
|
||||
"Copy Prompt" button at the bottom. Copy the generated prompt, paste
|
||||
it back here, and Claude revises brief.md from your notes.
|
||||
────────────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
> **Removed in v5.0.0.** v4.2/v4.3 shipped a ~388 KB bespoke playground SPA +
|
||||
> `/trekrevise` + Handover 8 (annotation → revision). A browser walkthrough
|
||||
> found it borderline unusable, and it duplicated the official `/playground`
|
||||
> plugin's `document-critique` / `diff-review` templates. All of it — the SPA,
|
||||
> the command, the supporting `lib/` modules, the anchor parser, the Playwright
|
||||
> e2e suite — was deleted. See [CHANGELOG.md](CHANGELOG.md) § v5.0.0.
|
||||
You copy that one `/playground …` line, paste it into Claude, and the
|
||||
official `claude-plugins-official` `playground` skill takes over: it loads
|
||||
its `document-critique` template, reads the `.md`, analyses it, generates
|
||||
per-line suggestions, and writes a single self-contained HTML file that
|
||||
opens in your browser. In the browser:
|
||||
|
||||
- **Left:** the artifact, nicely formatted, with line numbers and per-line
|
||||
highlight bars for any line a suggestion targets.
|
||||
- **Right:** suggestion cards (Approve / Reject / Comment), filterable by
|
||||
status, with a counter in the header.
|
||||
- **Bottom:** a live-updated prompt covering everything you approved or
|
||||
commented on, with a **Copy Prompt** button.
|
||||
|
||||
You mark what you want, click Copy Prompt, paste it back into Claude, and
|
||||
Claude revises the `.md` artifact freehand from your notes. **One paste
|
||||
→ playground → mark → copy → paste back.**
|
||||
|
||||
> **What v5.0.1 changed from v5.0.0.** v5.0.0 had `/trekbrief`, `/trekplan`,
|
||||
> and `/trekreview` each render their `.md` to a separate read-only `.html`
|
||||
> view via `scripts/render-artifact.mjs` *and* print a vague "run the
|
||||
> `/playground` plugin" instruction. In practice the read-only `.html` was
|
||||
> redundant with what `/playground` produces (which also shows the artifact
|
||||
> nicely formatted), and the instruction wasn't copy-paste-ready — operators
|
||||
> had to guess the right invocation. v5.0.1 drops `render-artifact.mjs` and
|
||||
> its `.html` output, and makes the printed `/playground` invocation
|
||||
> literal and copy-paste-ready.
|
||||
>
|
||||
> **Still removed from v5.0.0 onward:** the v4.2/v4.3 bespoke playground SPA,
|
||||
> `/trekrevise`, Handover 8 (annotation → revision), the supporting `lib/`
|
||||
> modules, and the Playwright e2e suite. See [CHANGELOG.md](CHANGELOG.md)
|
||||
> § v5.0.0 and § v5.0.1.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -667,7 +685,7 @@ The `pre-compact-flush.mjs` hook directly fixes the documented P0 in `docs/treke
|
|||
|
||||
**Infrastructure-as-code (IaC) gets reduced value.** The exploration agents are designed for application code. Terraform, Helm, Pulumi, CDK projects will get a plan, but agents like `architecture-mapper` and `test-strategist` produce less useful output for IaC. Use trekplan for the structural plan, then supplement IaC-specific steps manually.
|
||||
|
||||
**Rendered HTML is read-only.** `scripts/render-artifact.mjs` produces a static, self-contained view for browsing — it is not an editor. To revise an artifact from operator feedback, run the `/playground` plugin (`document-critique`) on the `.md` and paste its prompt back. The markdown subset the renderer supports covers what the artifact templates emit (headings, lists, code fences, tables, links, blockquotes, bold/italic, inline code); exotic markdown extensions are not rendered.
|
||||
**Annotation requires the official `/playground` skill.** Voyage no longer ships an annotation UI of its own. The pasted `/playground build a document-critique playground for …` command depends on the `claude-plugins-official` `playground` skill being installed in the operator's environment. If it isn't, paste the artifact source `.md` into Claude with a "review this" prompt and revise freehand — the same end-result, just without the visual approve/reject/comment surface.
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
|
|||
|
|
@ -483,30 +483,35 @@ If the validator returns errors, report them to the user and offer to
|
|||
re-enter Phase 4 with the validator's hints in scope. If only warnings,
|
||||
note them in the final report.
|
||||
|
||||
**Render to HTML + link (annotation via /playground):** after `brief.md`
|
||||
is final, render it to a self-contained HTML view in the same directory:
|
||||
**Report — single block, printed verbatim** (substitute the bracketed
|
||||
fields, but keep the structure and the `/playground` invocation literal —
|
||||
operators copy-paste that line directly into Claude). The `/playground`
|
||||
command points at the official `claude-plugins-official` `playground`
|
||||
skill, which loads its `document-critique` template, reads the brief,
|
||||
generates per-line suggestions, and writes a single self-contained HTML
|
||||
file that opens in the browser. The HTML has the brief on the left
|
||||
(nicely formatted, line-numbered), suggestion cards on the right
|
||||
(Approve / Reject / Comment), and a "Copy Prompt" button at the bottom
|
||||
that gathers everything marked into one prompt. Paste that prompt back
|
||||
into Claude — Claude then revises `brief.md` freehand from the notes.
|
||||
|
||||
```bash
|
||||
node ${CLAUDE_PLUGIN_ROOT}/scripts/render-artifact.mjs "{PROJECT_DIR}/brief.md"
|
||||
```
|
||||
|
||||
This writes `{PROJECT_DIR}/brief.html` — zero-network, design-system-styled
|
||||
(frontmatter folded into a `<details>` block). If it exits non-zero, surface
|
||||
a one-line warning and continue — the rendered view is a convenience, not a
|
||||
gate.
|
||||
|
||||
Report:
|
||||
```
|
||||
Brief written: {PROJECT_DIR}/brief.md
|
||||
Brief rendered: file://{abs path to brief.html}
|
||||
Brief written: {PROJECT_DIR}/brief.md
|
||||
Review iterations: {1..3}
|
||||
Final quality: {complete | partial}
|
||||
Validator: {PASS | warnings(N)}
|
||||
Final quality: {complete | partial}
|
||||
Validator: {PASS | warnings(N)}
|
||||
Research topics identified: {N}
|
||||
|
||||
To annotate: open brief.html, then run the `/playground` plugin
|
||||
(document-critique template) on brief.md and paste the generated
|
||||
prompt back here. Claude revises brief.md freehand from your notes.
|
||||
────────────────────────────────────────────────────────────────────
|
||||
To review and annotate this brief, copy and paste this into Claude:
|
||||
|
||||
/playground build a document-critique playground for {PROJECT_DIR}/brief.md
|
||||
|
||||
That builds a self-contained HTML file with the brief on the left,
|
||||
per-line approve/reject/comment annotations on the right, and a
|
||||
"Copy Prompt" button at the bottom. Copy the generated prompt, paste
|
||||
it back here, and Claude revises brief.md from your notes.
|
||||
────────────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
## Phase 5 — Auto-orchestration opt-in (if research_topics > 0)
|
||||
|
|
|
|||
|
|
@ -769,28 +769,32 @@ If the user asks questions or requests changes:
|
|||
- Show what changed
|
||||
- Re-present the summary
|
||||
|
||||
### Render to HTML + link (annotation via /playground)
|
||||
### Print the annotation invocation
|
||||
|
||||
After `plan.md` is final, render it to a self-contained HTML view in the
|
||||
same project directory and print the `file://` link:
|
||||
|
||||
```bash
|
||||
node ${CLAUDE_PLUGIN_ROOT}/scripts/render-artifact.mjs "{plan_path}"
|
||||
```
|
||||
|
||||
This writes `{plan_dir}/plan.html` — a zero-network, design-system-styled
|
||||
page (frontmatter folded into a `<details>` block, code fences in styled
|
||||
`<pre>`). Print:
|
||||
After the plan summary, print this block **verbatim** (substituting only
|
||||
`{plan_path}` with the absolute path). The `/playground` command must
|
||||
appear literally — operators copy-paste it directly into Claude. It
|
||||
points at the official `claude-plugins-official` `playground` skill,
|
||||
which loads its `document-critique` template, reads `plan.md`, generates
|
||||
per-line suggestions, and writes a single self-contained HTML file that
|
||||
opens in the browser. The HTML has the plan on the left (nicely
|
||||
formatted, line-numbered), suggestion cards on the right (Approve /
|
||||
Reject / Comment), and a "Copy Prompt" button at the bottom that gathers
|
||||
everything marked into one prompt. Paste that prompt back into Claude —
|
||||
Claude then revises `plan.md` freehand from the notes.
|
||||
|
||||
```
|
||||
Plan rendered: file://{abs path to plan.html}
|
||||
To annotate: open it, then run the `/playground` plugin
|
||||
(document-critique template) on plan.md and paste the generated
|
||||
prompt back here. Claude revises plan.md freehand from your notes.
|
||||
```
|
||||
────────────────────────────────────────────────────────────────────
|
||||
To review and annotate this plan, copy and paste this into Claude:
|
||||
|
||||
If `render-artifact.mjs` exits non-zero, surface a one-line warning and
|
||||
continue — the rendered view is a convenience, not a gate.
|
||||
/playground build a document-critique playground for {plan_path}
|
||||
|
||||
That builds a self-contained HTML file with the plan on the left,
|
||||
per-line approve/reject/comment annotations on the right, and a
|
||||
"Copy Prompt" button at the bottom. Copy the generated prompt, paste
|
||||
it back here, and Claude revises plan.md from your notes.
|
||||
────────────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
## Phase 11 — Handoff
|
||||
|
||||
|
|
|
|||
|
|
@ -262,17 +262,6 @@ Append a stats line to `${CLAUDE_PLUGIN_DATA}/trekreview-stats.jsonl`
|
|||
If `${CLAUDE_PLUGIN_DATA}` is unset or not writable, skip stats silently.
|
||||
Never let stats failures block the main workflow.
|
||||
|
||||
**Render to HTML + link (annotation via /playground):** after `review.md`
|
||||
is final, render it to a self-contained HTML view in the same directory:
|
||||
|
||||
```bash
|
||||
node ${CLAUDE_PLUGIN_ROOT}/scripts/render-artifact.mjs "{review_path}"
|
||||
```
|
||||
|
||||
This writes `{project_dir}/review.html` — zero-network, design-system-styled.
|
||||
If it exits non-zero, surface a one-line warning and continue — the rendered
|
||||
view is a convenience, not a gate.
|
||||
|
||||
## Phase 8.5 — Validate-only mode (`--validate`)
|
||||
|
||||
When `mode == validate`:
|
||||
|
|
@ -293,7 +282,6 @@ After the write succeeds, print:
|
|||
**Brief:** {brief_path}
|
||||
**Project:** {project_dir}
|
||||
**Review:** {review_path}
|
||||
**Rendered:** file://{abs path to review.html}
|
||||
**Scope:** {before_sha}..{after_sha} ({reviewed_files_count} files)
|
||||
**Verdict:** {BLOCK | WARN | ALLOW}
|
||||
|
||||
|
|
@ -308,12 +296,20 @@ After the write succeeds, print:
|
|||
...
|
||||
{up to 5 highest-severity findings}
|
||||
|
||||
You can:
|
||||
- Read the full review at {review_path} (or open review.html in a browser)
|
||||
────────────────────────────────────────────────────────────────────
|
||||
To review and annotate this review, copy and paste this into Claude:
|
||||
|
||||
/playground build a document-critique playground for {review_path}
|
||||
|
||||
That builds a self-contained HTML file with the review on the left,
|
||||
per-line approve/reject/comment annotations on the right, and a
|
||||
"Copy Prompt" button at the bottom. Copy the generated prompt, paste
|
||||
it back here, and Claude revises review.md from your notes.
|
||||
────────────────────────────────────────────────────────────────────
|
||||
|
||||
You can also:
|
||||
- Feed BLOCKER + MAJOR findings into a follow-up plan:
|
||||
/trekplan --brief {review_path}
|
||||
- Annotate: run the `/playground` plugin (document-critique template) on
|
||||
review.md and paste the generated prompt back here
|
||||
- Re-run with `--quick` for a faster correctness-only pass
|
||||
- Re-run with `--since <ref>` to narrow scope
|
||||
```
|
||||
|
|
|
|||
4
plugins/voyage/package-lock.json
generated
4
plugins/voyage/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "voyage",
|
||||
"version": "5.0.0",
|
||||
"version": "5.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "voyage",
|
||||
"version": "5.0.0",
|
||||
"version": "5.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
{
|
||||
"name": "voyage",
|
||||
"version": "5.0.0",
|
||||
"description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline; renders produced artifacts to HTML + link, annotate via the /playground plugin.",
|
||||
"version": "5.0.1",
|
||||
"description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline. At the end of /trekbrief, /trekplan, and /trekreview, the operator gets a copy-paste-ready /playground invocation that builds an interactive document-critique HTML for the artifact.",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node --test 'tests/**/*.test.mjs'",
|
||||
"render": "node scripts/render-artifact.mjs",
|
||||
"verify": "bash verify.sh"
|
||||
},
|
||||
"keywords": [
|
||||
|
|
|
|||
|
|
@ -1,321 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
// scripts/render-artifact.mjs
|
||||
//
|
||||
// Renders a voyage artifact (brief.md / plan.md / review.md) to a
|
||||
// self-contained HTML file in the same directory, with inlined CSS and
|
||||
// zero external network references. The producing commands (/trekbrief,
|
||||
// /trekplan, /trekreview) call this at the end and print the file:// link
|
||||
// so the operator can read the artifact in a browser — and, when they want
|
||||
// to annotate it, run the official `/playground` plugin (document-critique
|
||||
// template) on it and paste the generated prompt back into Claude Code.
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/render-artifact.mjs <artifact.md> [--out <output.html>]
|
||||
//
|
||||
// Determinism: no timestamps, no random IDs — two runs on the same input
|
||||
// produce byte-identical output.
|
||||
//
|
||||
// Zero npm deps (marketplace convention). The markdown→HTML conversion is a
|
||||
// small hand-rolled subset that covers what the artifact templates emit:
|
||||
// ATX headings, ordered/unordered/nested lists, fenced code blocks, inline
|
||||
// code, bold, links, blockquotes, GitHub-style tables, and horizontal rules.
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
||||
import { basename } from 'node:path';
|
||||
import { splitFrontmatter } from '../lib/util/frontmatter.mjs';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// Inline spans, applied to already-HTML-escaped text. Order matters: code
|
||||
// spans first (so their contents aren't re-processed), then links, bold, em.
|
||||
function renderInline(escaped) {
|
||||
let out = escaped.replace(/`([^`]+)`/g, (_, c) => `<code>${c}</code>`);
|
||||
out = out.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, text, href) => {
|
||||
const safe = /^(https?:|mailto:|#|\.|\/)/i.test(href) ? href : '#';
|
||||
return `<a href="${safe}">${text}</a>`;
|
||||
});
|
||||
out = out.replace(/\*\*([^*]+)\*\*/g, (_, c) => `<strong>${c}</strong>`);
|
||||
out = out.replace(/(^|[^*])\*([^*\s][^*]*?)\*(?!\*)/g, (_, pre, c) => `${pre}<em>${c}</em>`);
|
||||
return out;
|
||||
}
|
||||
|
||||
function renderTable(rows) {
|
||||
const cells = (line) => line.replace(/^\s*\|/, '').replace(/\|\s*$/, '').split('|').map((c) => c.trim());
|
||||
const header = cells(rows[0]);
|
||||
const body = rows.slice(2).map(cells);
|
||||
let html = '<table>\n<thead><tr>';
|
||||
for (const h of header) html += `<th>${renderInline(escapeHtml(h))}</th>`;
|
||||
html += '</tr></thead>\n<tbody>\n';
|
||||
for (const r of body) {
|
||||
html += '<tr>';
|
||||
for (let i = 0; i < header.length; i++) html += `<td>${renderInline(escapeHtml(r[i] || ''))}</td>`;
|
||||
html += '</tr>\n';
|
||||
}
|
||||
return html + '</tbody>\n</table>\n';
|
||||
}
|
||||
|
||||
// Build nested <ul>/<ol> from a run of list lines (2-space indent = 1 level).
|
||||
function renderList(items) {
|
||||
let html = '';
|
||||
const stack = []; // { indent, ordered }
|
||||
for (const { indent, ordered, text } of items) {
|
||||
while (
|
||||
stack.length &&
|
||||
(indent < stack[stack.length - 1].indent ||
|
||||
(indent === stack[stack.length - 1].indent && ordered !== stack[stack.length - 1].ordered))
|
||||
) {
|
||||
const top = stack.pop();
|
||||
html += top.ordered ? '</li></ol>' : '</li></ul>';
|
||||
}
|
||||
if (!stack.length || indent > stack[stack.length - 1].indent) {
|
||||
html += ordered ? '<ol>' : '<ul>';
|
||||
stack.push({ indent, ordered });
|
||||
} else {
|
||||
html += '</li>';
|
||||
}
|
||||
html += `<li>${renderInline(escapeHtml(text))}`;
|
||||
}
|
||||
while (stack.length) {
|
||||
const top = stack.pop();
|
||||
html += top.ordered ? '</li></ol>' : '</li></ul>';
|
||||
}
|
||||
return html + '\n';
|
||||
}
|
||||
|
||||
function renderMarkdown(md) {
|
||||
const lines = md.replace(/\r\n/g, '\n').split('\n');
|
||||
let html = '';
|
||||
let i = 0;
|
||||
let para = [];
|
||||
|
||||
const flushPara = () => {
|
||||
if (para.length) {
|
||||
html += `<p>${renderInline(escapeHtml(para.join(' ')))}</p>\n`;
|
||||
para = [];
|
||||
}
|
||||
};
|
||||
|
||||
while (i < lines.length) {
|
||||
const line = lines[i];
|
||||
|
||||
// Fenced code block
|
||||
const fence = line.match(/^(\s*)(`{3,}|~{3,})(.*)$/);
|
||||
if (fence) {
|
||||
flushPara();
|
||||
const marker = fence[2];
|
||||
const lang = (fence[3] || '').trim().split(/\s+/)[0];
|
||||
const buf = [];
|
||||
i++;
|
||||
while (i < lines.length && !lines[i].match(new RegExp('^\\s*' + marker[0] + '{3,}\\s*$'))) {
|
||||
buf.push(lines[i]);
|
||||
i++;
|
||||
}
|
||||
i++; // consume closing fence
|
||||
const cls = lang ? ` class="language-${escapeHtml(lang)}"` : '';
|
||||
html += `<pre><code${cls}>${escapeHtml(buf.join('\n'))}\n</code></pre>\n`;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ATX heading
|
||||
const h = line.match(/^(#{1,6})\s+(.*?)\s*#*\s*$/);
|
||||
if (h) {
|
||||
flushPara();
|
||||
const lvl = h[1].length;
|
||||
html += `<h${lvl}>${renderInline(escapeHtml(h[2]))}</h${lvl}>\n`;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Horizontal rule
|
||||
if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
|
||||
flushPara();
|
||||
html += '<hr>\n';
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Table (header row + separator row)
|
||||
if (/^\s*\|.*\|\s*$/.test(line) && i + 1 < lines.length && /^\s*\|?[\s:|-]+\|?\s*$/.test(lines[i + 1]) && lines[i + 1].includes('-')) {
|
||||
flushPara();
|
||||
const rows = [];
|
||||
while (i < lines.length && /^\s*\|.*\|\s*$/.test(lines[i])) { rows.push(lines[i]); i++; }
|
||||
// include the separator that was matched as part of rows already
|
||||
html += renderTable(rows);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Blockquote
|
||||
if (/^\s*>\s?/.test(line)) {
|
||||
flushPara();
|
||||
const buf = [];
|
||||
while (i < lines.length && /^\s*>\s?/.test(lines[i])) { buf.push(lines[i].replace(/^\s*>\s?/, '')); i++; }
|
||||
html += `<blockquote>${renderInline(escapeHtml(buf.join(' ')))}</blockquote>\n`;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Lists (consume a contiguous block, allowing blank lines between items)
|
||||
const listMatch = line.match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
|
||||
if (listMatch) {
|
||||
flushPara();
|
||||
const items = [];
|
||||
while (i < lines.length) {
|
||||
const m = lines[i].match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
|
||||
if (m) {
|
||||
items.push({ indent: m[1].length, ordered: /\d/.test(m[2]), text: m[3] });
|
||||
i++;
|
||||
} else if (lines[i].trim() === '' && i + 1 < lines.length && lines[i + 1].match(/^(\s*)([-*+]|\d+[.)])\s+/)) {
|
||||
i++; // blank line inside the list
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
html += renderList(items);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Blank line — paragraph break
|
||||
if (line.trim() === '') {
|
||||
flushPara();
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Default — accumulate into paragraph
|
||||
para.push(line.trim());
|
||||
i++;
|
||||
}
|
||||
flushPara();
|
||||
return html;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const STYLE = `
|
||||
:root { color-scheme: light; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; padding: 2.5rem 1.25rem 4rem;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px; line-height: 1.6; color: #1a1a1a; background: #f7f7f8;
|
||||
}
|
||||
main { max-width: 56rem; margin: 0 auto; background: #fff; border: 1px solid #e2e2e6;
|
||||
border-radius: 12px; padding: 2.5rem 3rem; box-shadow: 0 1px 3px rgba(0,0,0,.06); }
|
||||
h1, h2, h3, h4, h5, h6 { line-height: 1.25; margin: 1.8em 0 .6em; font-weight: 650; }
|
||||
h1 { font-size: 2rem; margin-top: 0; }
|
||||
h2 { font-size: 1.5rem; border-bottom: 1px solid #ececef; padding-bottom: .3em; }
|
||||
h3 { font-size: 1.2rem; }
|
||||
h4 { font-size: 1.05rem; }
|
||||
p { margin: .8em 0; }
|
||||
a { color: #0855a8; text-decoration: underline; text-underline-offset: 2px; }
|
||||
a:hover { color: #06408a; }
|
||||
code { font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
|
||||
font-size: .9em; background: #f0f0f3; padding: .12em .35em; border-radius: 4px; }
|
||||
pre { background: #1e1e24; color: #e6e6eb; padding: 1rem 1.25rem; border-radius: 8px;
|
||||
overflow-x: auto; font-size: .85rem; line-height: 1.5; }
|
||||
pre code { background: none; padding: 0; color: inherit; font-size: inherit; }
|
||||
blockquote { margin: 1em 0; padding: .4em 1.2em; border-left: 4px solid #0855a8;
|
||||
background: #f0f5fb; color: #34495e; border-radius: 0 6px 6px 0; }
|
||||
ul, ol { padding-left: 1.6em; margin: .8em 0; }
|
||||
li { margin: .25em 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 1.2em 0; font-size: .92rem; }
|
||||
th, td { border: 1px solid #e2e2e6; padding: .5em .75em; text-align: left; vertical-align: top; }
|
||||
th { background: #f0f0f3; font-weight: 600; }
|
||||
tr:nth-child(even) td { background: #fafafb; }
|
||||
hr { border: none; border-top: 1px solid #e2e2e6; margin: 2em 0; }
|
||||
details.frontmatter { margin: 0 0 2rem; border: 1px solid #e2e2e6; border-radius: 8px;
|
||||
background: #fafafb; padding: .6em 1em; }
|
||||
details.frontmatter > summary { cursor: pointer; font-weight: 600; font-size: .9rem; color: #555; }
|
||||
details.frontmatter pre { margin: .8em 0 .2em; background: #f4f4f6; color: #333; }
|
||||
.artifact-meta { color: #888; font-size: .82rem; margin: 0 0 1.5rem; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { color-scheme: dark; }
|
||||
body { color: #e6e6eb; background: #18181b; }
|
||||
main { background: #1f1f23; border-color: #2e2e34; box-shadow: none; }
|
||||
h2 { border-bottom-color: #2e2e34; }
|
||||
a { color: #6db0ee; } a:hover { color: #93c5fd; }
|
||||
code { background: #2a2a30; }
|
||||
blockquote { background: #1a242f; color: #b6c5d4; border-left-color: #6db0ee; }
|
||||
th, td { border-color: #2e2e34; } th { background: #26262c; }
|
||||
tr:nth-child(even) td { background: #222226; }
|
||||
hr { border-top-color: #2e2e34; }
|
||||
details.frontmatter { background: #222226; border-color: #2e2e34; }
|
||||
details.frontmatter > summary { color: #aaa; }
|
||||
details.frontmatter pre { background: #1a1a1d; color: #ccc; }
|
||||
.artifact-meta { color: #777; }
|
||||
}
|
||||
@media print { body { background: #fff; padding: 0; } main { border: none; box-shadow: none; max-width: none; } }
|
||||
`.trim();
|
||||
|
||||
function buildHtml(mdPath, mdText) {
|
||||
const { hasFrontmatter, frontmatter, body } = splitFrontmatter(mdText);
|
||||
const fm = hasFrontmatter ? frontmatter : '';
|
||||
const fmLine = (key) => {
|
||||
const m = fm.match(new RegExp('^' + key + ':\\s*(.+)$', 'm'));
|
||||
return m ? m[1].trim().replace(/^["']|["']$/g, '') : null;
|
||||
};
|
||||
const title = fmLine('task') || fmLine('slug') || (body.match(/^#\s+(.+)$/m) || [])[1] || basename(mdPath);
|
||||
const kind = fmLine('type') || basename(mdPath).replace(/\.md$/, '');
|
||||
|
||||
const fmBlock = hasFrontmatter
|
||||
? `<details class="frontmatter"><summary>Frontmatter</summary><pre><code>${escapeHtml(fm)}\n</code></pre></details>\n`
|
||||
: '';
|
||||
|
||||
const bodyHtml = renderMarkdown(body);
|
||||
|
||||
return '<!DOCTYPE html>\n'
|
||||
+ '<html lang="en">\n<head>\n<meta charset="utf-8">\n'
|
||||
+ '<meta name="viewport" content="width=device-width, initial-scale=1">\n'
|
||||
+ `<title>${escapeHtml(String(title))}</title>\n`
|
||||
+ `<style>\n${STYLE}\n</style>\n</head>\n<body>\n<main>\n`
|
||||
+ `<p class="artifact-meta">voyage artifact — ${escapeHtml(String(kind))}</p>\n`
|
||||
+ fmBlock
|
||||
+ bodyHtml
|
||||
+ '</main>\n</body>\n</html>\n';
|
||||
}
|
||||
|
||||
function render(inputPath, outputPath) {
|
||||
if (!existsSync(inputPath)) {
|
||||
process.stderr.write(`render-artifact: input not found: ${inputPath}\n`);
|
||||
process.exit(2);
|
||||
}
|
||||
const text = readFileSync(inputPath, 'utf-8');
|
||||
const html = buildHtml(inputPath, text);
|
||||
const out = outputPath || inputPath.replace(/\.md$/, '.html');
|
||||
writeFileSync(out, html);
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { input: null, out: null, help: false };
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === '--out') args.out = argv[++i];
|
||||
else if (a === '--help' || a === '-h') args.help = true;
|
||||
else if (!args.input) args.input = a;
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (args.help || !args.input) {
|
||||
process.stdout.write(
|
||||
'Usage: render-artifact <artifact.md> [--out <output.html>]\n\n'
|
||||
+ 'Renders a voyage artifact to a self-contained HTML file (zero network).\n'
|
||||
+ 'Default output: <input-basename>.html next to the input.\n',
|
||||
);
|
||||
process.exit(args.help ? 0 : 2);
|
||||
}
|
||||
const out = render(args.input, args.out);
|
||||
process.stdout.write(out + '\n');
|
||||
}
|
||||
|
||||
export { render, buildHtml, renderMarkdown, parseArgs };
|
||||
|
|
@ -400,13 +400,13 @@ test('commands/trekplan.md Phase 8 seals Opus-4.7 schema-drift defense', () => {
|
|||
);
|
||||
});
|
||||
|
||||
// --- v5.0.0 — bespoke playground + /trekrevise + Handover 8 removed ---
|
||||
// --- v5.0.0 / v5.0.1 — bespoke playground removed; /playground invocation explicit ---
|
||||
//
|
||||
// The v4.2/v4.3 bespoke playground SPA, the /trekrevise command, and
|
||||
// Handover 8 (annotation → revision) were removed in v5.0.0. Producing
|
||||
// commands now render artifacts to self-contained HTML via
|
||||
// scripts/render-artifact.mjs and direct operators at the official
|
||||
// `/playground` plugin for annotation. These pins lock the removal in.
|
||||
// v5.0.0 removed the bespoke playground SPA, /trekrevise, and Handover 8.
|
||||
// v5.0.1 dropped the v5.0.0 stop-gap (scripts/render-artifact.mjs) and made
|
||||
// the producing commands print a literal, copy-paste-ready /playground
|
||||
// document-critique invocation instead. These pins lock both removals in
|
||||
// AND pin the new copy-paste invocation as the operator-facing contract.
|
||||
|
||||
import { existsSync } from 'node:fs';
|
||||
|
||||
|
|
@ -430,36 +430,55 @@ test('Handover 8 deleted from HANDOVER-CONTRACTS.md (back to seven handovers)',
|
|||
assert.ok(text.includes('## Handover 7'), 'Handover 7 must remain');
|
||||
});
|
||||
|
||||
test('scripts/render-artifact.mjs exists (v5.0.0 render-and-link step)', () => {
|
||||
test('scripts/render-artifact.mjs no longer exists (removed in v5.0.1)', () => {
|
||||
assert.ok(
|
||||
existsSync(join(ROOT, 'scripts/render-artifact.mjs')),
|
||||
'scripts/render-artifact.mjs is required — producing commands call it to render artifacts to HTML',
|
||||
!existsSync(join(ROOT, 'scripts/render-artifact.mjs')),
|
||||
'scripts/render-artifact.mjs should be deleted — v5.0.1 drops the redundant standalone HTML render in favour of the /playground document-critique invocation printed by the producing commands',
|
||||
);
|
||||
});
|
||||
|
||||
test('producing commands reference render-artifact.mjs (render-and-link step)', () => {
|
||||
test('producing commands print a literal /playground document-critique invocation', () => {
|
||||
// The exact substring must appear in each producing command's prose so the
|
||||
// operator copy-pastes a verbatim line. Drift on this is the friction point
|
||||
// that motivated v5.0.1 — fail loudly if the prose softens back to "run the
|
||||
// /playground plugin" without the literal command.
|
||||
const REQUIRED = '/playground build a document-critique playground for';
|
||||
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
|
||||
assert.ok(
|
||||
read(`commands/${f}`).includes('render-artifact.mjs'),
|
||||
`commands/${f} must wire the render-artifact.mjs render-and-link step (v5.0.0)`,
|
||||
read(`commands/${f}`).includes(REQUIRED),
|
||||
`commands/${f} must include the literal invocation "${REQUIRED}" so the operator copy-pastes it directly (v5.0.1)`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('producing commands point operators at the /playground plugin for annotation', () => {
|
||||
test('producing commands no longer reference the removed scripts/render-artifact.mjs', () => {
|
||||
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
|
||||
assert.ok(
|
||||
read(`commands/${f}`).includes('/playground'),
|
||||
`commands/${f} must mention the /playground plugin as the annotation path (v5.0.0)`,
|
||||
!read(`commands/${f}`).includes('render-artifact.mjs'),
|
||||
`commands/${f} still references scripts/render-artifact.mjs — that script was removed in v5.0.1`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('package.json no longer has an "npm run render" script (removed in v5.0.1)', () => {
|
||||
const pkg = JSON.parse(read('package.json'));
|
||||
assert.equal(
|
||||
pkg.scripts && pkg.scripts.render,
|
||||
undefined,
|
||||
'package.json scripts.render should be gone in v5.0.1',
|
||||
);
|
||||
});
|
||||
|
||||
test('CHANGELOG.md has v5.0.0 entry', () => {
|
||||
const cl = read('CHANGELOG.md');
|
||||
assert.match(cl, /## v5\.0\.0\b/, 'CHANGELOG.md must include "## v5.0.0" entry');
|
||||
});
|
||||
|
||||
test('CHANGELOG.md has v5.0.1 entry', () => {
|
||||
const cl = read('CHANGELOG.md');
|
||||
assert.match(cl, /## v5\.0\.1\b/, 'CHANGELOG.md must include "## v5.0.1" entry');
|
||||
});
|
||||
|
||||
test('CHANGELOG.md retains v4.2.0 entry (history is not rewritten)', () => {
|
||||
const cl = read('CHANGELOG.md');
|
||||
assert.match(cl, /## v4\.2\.0\b/, 'CHANGELOG.md must keep the historical "## v4.2.0" entry');
|
||||
|
|
|
|||
|
|
@ -1,122 +0,0 @@
|
|||
// tests/scripts/render-artifact.test.mjs
|
||||
// Covers scripts/render-artifact.mjs — the v5.0.0 self-contained HTML
|
||||
// renderer that /trekbrief, /trekplan, /trekreview call at the end of their
|
||||
// run to produce a browser-readable view of the just-written artifact.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { mkdtempSync, writeFileSync, readFileSync, rmSync, existsSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { buildHtml, renderMarkdown, render, parseArgs } from '../../scripts/render-artifact.mjs';
|
||||
|
||||
const SAMPLE = `---
|
||||
type: trekplan
|
||||
plan_version: "1.7"
|
||||
task: "Render-artifact smoke test"
|
||||
slug: render-smoke
|
||||
---
|
||||
|
||||
# Render-artifact smoke test
|
||||
|
||||
A paragraph with **bold**, \`inline code\`, and a [link](https://example.com).
|
||||
|
||||
## Steps
|
||||
|
||||
- top item
|
||||
- nested item
|
||||
- second top item
|
||||
|
||||
1. ordered one
|
||||
2. ordered two
|
||||
|
||||
\`\`\`js
|
||||
const x = 1;
|
||||
\`\`\`
|
||||
|
||||
> a blockquote line
|
||||
|
||||
| Col A | Col B |
|
||||
|-------|-------|
|
||||
| 1 | 2 |
|
||||
`;
|
||||
|
||||
test('buildHtml produces a complete self-contained HTML document', () => {
|
||||
const html = buildHtml('plan.md', SAMPLE);
|
||||
assert.ok(html.startsWith('<!DOCTYPE html>'), 'must start with doctype');
|
||||
assert.ok(html.includes('</html>'), 'must close html');
|
||||
assert.ok(html.includes('<style>'), 'must inline a stylesheet');
|
||||
// Zero external network references.
|
||||
assert.ok(!/<link[^>]+href=/i.test(html), 'no external <link> stylesheets');
|
||||
assert.ok(!/<script[^>]+src=/i.test(html), 'no external <script src>');
|
||||
assert.ok(!/https?:\/\/(?!example\.com)/.test(html.replace(/<style>[\s\S]*?<\/style>/, '')), 'no unexpected http(s) URLs outside example link');
|
||||
});
|
||||
|
||||
test('buildHtml folds frontmatter into a <details> block', () => {
|
||||
const html = buildHtml('plan.md', SAMPLE);
|
||||
assert.ok(html.includes('<details class="frontmatter">'), 'frontmatter wrapped in <details>');
|
||||
assert.ok(html.includes('plan_version'), 'frontmatter content preserved');
|
||||
// Frontmatter must NOT leak into the rendered body as a literal "---" rule.
|
||||
const bodyOnly = html.split('</details>')[1] || '';
|
||||
assert.ok(!bodyOnly.startsWith('\n<hr>'), 'frontmatter fence should not become an <hr>');
|
||||
});
|
||||
|
||||
test('buildHtml derives the <title> from frontmatter task', () => {
|
||||
const html = buildHtml('plan.md', SAMPLE);
|
||||
assert.match(html, /<title>Render-artifact smoke test<\/title>/);
|
||||
});
|
||||
|
||||
test('renderMarkdown renders headings, code fences, lists, tables, blockquotes', () => {
|
||||
const out = renderMarkdown(SAMPLE.split('---\n').slice(2).join('---\n'));
|
||||
assert.match(out, /<h1>Render-artifact smoke test<\/h1>/);
|
||||
assert.match(out, /<h2>Steps<\/h2>/);
|
||||
assert.match(out, /<pre><code class="language-js">/);
|
||||
assert.ok(out.includes('const x = 1;'), 'code fence body preserved');
|
||||
assert.match(out, /<ul><li>top item<ul><li>nested item<\/li><\/ul><\/li>/);
|
||||
assert.match(out, /<ol><li>ordered one<\/li><li>ordered two<\/li><\/ol>/);
|
||||
assert.match(out, /<blockquote>a blockquote line<\/blockquote>/);
|
||||
assert.match(out, /<table>[\s\S]*<th>Col A<\/th>[\s\S]*<td>1<\/td>[\s\S]*<\/table>/);
|
||||
assert.match(out, /<strong>bold<\/strong>/);
|
||||
assert.match(out, /<code>inline code<\/code>/);
|
||||
assert.match(out, /<a href="https:\/\/example\.com">link<\/a>/);
|
||||
});
|
||||
|
||||
test('renderMarkdown escapes HTML in body and code', () => {
|
||||
const out = renderMarkdown('A <tag> & "quote".\n\n```\n<script>alert(1)</script>\n```\n');
|
||||
assert.ok(!out.includes('<tag>'), 'raw tag escaped');
|
||||
assert.ok(out.includes('<tag>'), 'tag rendered as entity');
|
||||
assert.ok(out.includes('<script>alert(1)</script>'), 'code-fence content escaped');
|
||||
});
|
||||
|
||||
test('render() is deterministic — two runs byte-identical', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'render-artifact-'));
|
||||
try {
|
||||
const md = join(dir, 'plan.md');
|
||||
writeFileSync(md, SAMPLE);
|
||||
const out1 = render(md, join(dir, 'a.html'));
|
||||
const out2 = render(md, join(dir, 'b.html'));
|
||||
assert.ok(existsSync(out1) && existsSync(out2));
|
||||
assert.equal(readFileSync(out1, 'utf-8'), readFileSync(out2, 'utf-8'));
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('render() defaults output to <input-basename>.html', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'render-artifact-'));
|
||||
try {
|
||||
const md = join(dir, 'review.md');
|
||||
writeFileSync(md, '# Review\n\nok\n');
|
||||
const out = render(md);
|
||||
assert.equal(out, join(dir, 'review.html'));
|
||||
assert.ok(existsSync(out));
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('parseArgs handles --out and positional input', () => {
|
||||
assert.deepEqual(parseArgs(['x.md']), { input: 'x.md', out: null, help: false });
|
||||
assert.deepEqual(parseArgs(['x.md', '--out', 'y.html']), { input: 'x.md', out: 'y.html', help: false });
|
||||
assert.equal(parseArgs(['--help']).help, true);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue