diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a355eb2..35ea29e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3782,3 +3782,61 @@ jobs: run: ci/check-visual-artifact-templates.sh - name: End-to-end render contract run: ci/e2e-visual-artifacts.sh + + visual-artifact-public-copy: + # Locks the public framing introduced in PR 5: visual artifacts are + # "inspectable local evidence", JSON canonical, HTML derived. No + # cloud/SaaS/certification language allowed inside any paragraph + # that mentions render-artifact.sh, visual artifacts, or + # .nanostack/visual. + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - name: Public copy is locally-framed and modest + run: | + set -e + # Files that may speak publicly about visual artifacts. + FILES=(README.md README.es.md llms.txt AGENTS.md reference/visual-artifact-contract.md) + # Words that misrepresent the local-only, optional nature of + # the renderer. The check pulls a 10-line window around every + # render-artifact / visual artifact mention so the lint only + # fires when the banned word actually appears in the context + # that frames the renderer publicly. + BAD_WORDS='cloud viewer|hosted viewer|SaaS viewer|cloud dashboard|cloud render|cloud-rendered|cloud-based viewer|attestation|certified release|enterprise[- ]grade' + fail=0 + for f in "${FILES[@]}"; do + [ -f "$f" ] || continue + # Extract a 10-line window around every renderer mention. + window=$(grep -niE -A10 -B2 'render-artifact|visual artifact|\.nanostack/visual' "$f" 2>/dev/null || true) + if [ -z "$window" ]; then continue; fi + if printf '%s\n' "$window" | grep -niE -- "$BAD_WORDS" >/dev/null 2>&1; then + echo "FAIL: $f has a banned framing word in a visual-artifact paragraph." + printf '%s\n' "$window" | grep -niE -- "$BAD_WORDS" + fail=1 + fi + done + [ "$fail" = "1" ] && exit 1 + echo "OK: visual artifact public copy stays local-first and modest." + + - name: Visual artifact section present in public surfaces + run: | + set -e + # The public surfaces must each at least *acknowledge* the + # renderer once so the docs do not drift back to JSON-only + # framing. Codex PR 5 anchor. + for pair in \ + "README.md|render-artifact" \ + "README.es.md|render-artifact" \ + "llms.txt|render-artifact" \ + "AGENTS.md|render-artifact" \ + "reference/visual-artifact-contract.md|render-artifact"; do + file="${pair%|*}" + needle="${pair#*|}" + if ! grep -qF "$needle" "$file"; then + echo "FAIL: $file does not mention $needle. Update the public copy when PR 5 framing changes." + exit 1 + fi + done + echo "OK: every public surface mentions render-artifact." diff --git a/AGENTS.md b/AGENTS.md index 35485ce..ea351fa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,6 +40,10 @@ Custom stacks declare their own phases in `.nanostack/config.json` (`custom_phas Skills automatically save artifacts to `.nanostack/`. Downstream skills read upstream artifacts through `bin/resolve.sh`, which honors the artifact-trust contract (PR 2) and the routing contract for custom skills (PR 5). `/ship` generates a sprint journal. `bin/discard-sprint.sh` cleans up bad sessions. +## Visual artifacts + +`bin/render-artifact.sh` produces local HTML views of any phase artifact, sprint journal, or custom stack DAG under `.nanostack/visual/`. JSON stays canonical; the HTML is a derived, deletable inspection layer. The renderer is offline (no external scripts, no fetch, no storage) and supports `--strict` for integrity-verified rendering plus `--interactive` for copy-only clipboard buttons on `/plan` and `/review`. Contract: `reference/visual-artifact-contract.md`. + ## Usage Each skill's `SKILL.md` contains the full instructions. Read it and follow the process described. Supporting files (templates, references, checklists, scripts) live in subdirectories and are referenced from the SKILL.md when needed. diff --git a/README.es.md b/README.es.md index 5f2b5b8..1de1e84 100644 --- a/README.es.md +++ b/README.es.md @@ -276,6 +276,28 @@ Un equipo de marketing arma `/audience` y `/campaign`. Un equipo de datos arma ` Walkthrough completo: [`EXTENDING.md`](EXTENDING.md). +## Artifactos visuales + +Cada artefacto de fase es JSON. El JSON es lo que leen los skills, lo que firma el hash de integridad y lo que agrega el sprint journal. Esa capa es la canónica. + +Encima de eso, `bin/render-artifact.sh` produce una vista HTML local de cualquier artefacto para que un humano pueda inspeccionar la misma evidencia en el navegador: + +```bash +bin/render-artifact.sh plan --latest # último /nano +bin/render-artifact.sh review --latest # /review con contadores de severidad +bin/render-artifact.sh security --latest # OWASP / STRIDE +bin/render-artifact.sh journal --today # timeline del sprint +bin/render-artifact.sh stack compliance-release # DAG del workflow custom +``` + +El output queda en `.nanostack/visual/` al lado del JSON que lo originó. Cada render escribe un manifest que registra path origen, integridad SHA-256, timestamp y versión del renderer. Podés borrar el HTML cuando quieras: el JSON no cambia y la vista se regenera. + +El renderer es offline: cada página trae su propio CSS, el Content-Security-Policy bloquea la red externa, no se cargan fonts ni scripts de CDN. El flag `--strict` falla si algún artefacto fuente tiene la integridad rota (`integrity_mismatch`) o sin firmar (`integrity_missing`). + +`--interactive` suma botones de copia en `/plan` y `/review` (copy as prompt, copy as Markdown, copy as JSON patch). Usan solo el clipboard local. Sin escrituras a disco, sin llamadas de red, sin form submission. + +Los artifactos visuales son una capa opcional de inspección. Nada depende de ellos: borrar `.nanostack/visual/` no cambia el comportamiento de ningún skill ni el estado del sprint. El contrato vive en `reference/visual-artifact-contract.md`. + ## Privacidad Nanostack no tiene un servicio cloud propio. Guarda planes, artefactos, journals y know-how localmente en `.nanostack/`. No envía tu código, prompts, nombres de proyecto ni rutas de archivo a servidores de Nanostack. Tu proveedor de agente de IA puede procesar el contexto que le des; usá las opciones de privacidad de tu proveedor y tus propias políticas de datos para trabajo sensible. diff --git a/README.md b/README.md index f4cf188..579e8e2 100644 --- a/README.md +++ b/README.md @@ -668,6 +668,28 @@ When you run `/ship` and the PR lands, it automatically generates a sprint journ The journal reads every phase artifact from the sprint and writes one file with the full decision trail: what `/think` reframed, what `/nano` scoped, what `/review` found, how conflicts were resolved, what `/security` graded. +### Visual artifacts + +Every phase artifact is JSON. JSON is what every skill reads, what trust verification hashes, what the sprint journal aggregates. That layer stays canonical. + +On top of it, `bin/render-artifact.sh` produces a local HTML view of any artifact so a human can inspect the same evidence in a browser: + +```bash +bin/render-artifact.sh plan --latest # render the latest plan +bin/render-artifact.sh review --latest # review with severity counters +bin/render-artifact.sh security --latest # OWASP / STRIDE breakdown +bin/render-artifact.sh journal --today # whole sprint timeline +bin/render-artifact.sh stack compliance-release # custom workflow DAG +``` + +Output lands under `.nanostack/visual/` next to the JSON it came from. Every render writes a companion manifest that records source path, source integrity, render timestamp, and renderer version. Delete a generated HTML file at any time; the JSON is unchanged and the view can be regenerated from it. + +The renderer is offline-only: every page ships its own CSS, the Content-Security-Policy header blocks external network, no fonts or scripts are loaded from a CDN. A `--strict` flag fails the render when any source artifact's SHA-256 integrity hash does not match (`integrity_mismatch`) or is missing (`integrity_missing`). + +`--interactive` adds copy-only buttons to `/plan` and `/review` views: copy as prompt, copy as Markdown, copy as JSON patch. The buttons use the local clipboard API only. No filesystem writes, no network calls, no form submission. + +Visual artifacts are an optional inspection layer. Nothing depends on them: removing `.nanostack/visual/` does not change skill behavior or sprint state. The contract lives in `reference/visual-artifact-contract.md`. + ### Knowledge compounding on /compound After shipping, run `/compound` to document what you learned: diff --git a/bin/about.sh b/bin/about.sh index 388d038..9133b56 100755 --- a/bin/about.sh +++ b/bin/about.sh @@ -68,6 +68,7 @@ Local workflow framework for AI coding agents. The built-in sprint plus a framew | bin/sprint-metrics.sh | Git stats + cycle time (used by /think --retro and /nano). | | bin/doctor.sh | Know-how health check. | | bin/capture-failure.sh | Log what went wrong (no /compound needed). | +| bin/render-artifact.sh | Render a local HTML view of any artifact under \`.nanostack/visual/\`. Optional, JSON stays canonical. | ## Custom workflow stacks @@ -80,6 +81,7 @@ All data in \`.nanostack/\`: - Solutions: \`.nanostack/know-how/solutions/{bug,pattern,decision}/\` - Briefs: \`.nanostack/know-how/briefs/\` - Audit log: \`.nanostack/audit.log\` +- Visual artifacts (optional): \`.nanostack/visual/\` (HTML views derived from JSON; safe to delete) There is no Nanostack cloud. Telemetry is opt-in and documented in \`TELEMETRY.md\`. diff --git a/llms.txt b/llms.txt index 59951c0..b6322ec 100644 --- a/llms.txt +++ b/llms.txt @@ -39,6 +39,10 @@ See `reference/custom-stack-contract.md` for the contract, `examples/custom-stac Every artifact written by `bin/save-artifact.sh` carries a SHA-256 integrity field. `bin/find-artifact.sh` has a `--require-integrity` flag for strict consumers, and `bin/resolve.sh` exposes per-upstream trust state in its `upstream_status` field (`verified`, `integrity_missing`, `integrity_mismatch`, `missing`, `not_applicable`). +## Visual artifacts + +`bin/render-artifact.sh` produces a local HTML view of any phase artifact, sprint journal, or custom stack DAG. Output lands under `.nanostack/visual/` with a companion manifest recording the source artifact path, integrity, and renderer version. HTML is a derived, deletable view; JSON remains canonical. The renderer is offline (Content-Security-Policy locks every page, no external scripts or fonts). A `--strict` flag fails the render when any source has `integrity_missing` or `integrity_mismatch`. An `--interactive` flag adds copy-only clipboard buttons to `/plan` and `/review` (copy as prompt / Markdown / JSON patch); no filesystem writes, no network calls. The contract is in `reference/visual-artifact-contract.md`. + ## Privacy There is no Nanostack cloud. Artifacts, journals, and analytics stay under `.nanostack/` on disk. Telemetry is opt-in and documented in `TELEMETRY.md`; the on-by-default behavior is no remote calls. diff --git a/reference/visual-artifact-contract.md b/reference/visual-artifact-contract.md index ac5ce3f..1c0d284 100644 --- a/reference/visual-artifact-contract.md +++ b/reference/visual-artifact-contract.md @@ -2,67 +2,79 @@ ## Purpose -`bin/render-artifact.sh` writes a static HTML view of a Nanostack JSON artifact. The HTML is a derived, local, inspectable view of the canonical artifact. JSON remains the source of truth; the renderer is strictly downstream. +`bin/render-artifact.sh` writes a static HTML view of a Nanostack JSON artifact, sprint journal, or custom stack DAG. The HTML is a derived, local, inspectable view of canonical evidence. JSON remains the source of truth; the renderer is strictly downstream. -This contract is normative for PR 1 of the Visual Artifacts Architecture v1 round (2026-05-11). It documents what the renderer produces, where it writes, what it refuses, and how downstream consumers must treat the output. +This contract is normative for the Visual Artifacts v1 round (2026-05-11). It documents what the renderer produces, where it writes, what it refuses, and how downstream consumers must treat the output. ## Hard invariants 1. **JSON is canonical.** No skill (`/review`, `/security`, `/qa`, `/ship`, `bin/resolve.sh`, conductor) reads HTML as source evidence. HTML can be deleted and regenerated without changing sprint state. -2. **Deterministic.** Same source artifact + same renderer version produces semantically equivalent HTML and manifest. Only the manifest `created_at` field is allowed to vary. -3. **Local.** Output paths live under `$NANOSTACK_STORE/visual/`. The renderer refuses `--out` paths that escape the visual root and refuses to follow a symlinked visual root. +2. **Deterministic.** Same source artifact + same renderer version produces semantically equivalent HTML and manifest. Only the manifest `created_at` and timestamp fields are allowed to vary. +3. **Local.** Output paths live under `$NANOSTACK_STORE/visual/`. The renderer refuses `--out` paths that escape the visual root and refuses to follow a symlinked visual root or any symlinked subdirectory under it. 4. **Trust-aware.** Every render records source trust. `--strict` rejects `integrity_missing` and `integrity_mismatch`. Without `--strict`, `integrity_mismatch` still fails (exit 3); `integrity_missing` renders with a visible untrusted badge. 5. **Escaped.** Every string read from JSON passes through `nano_html_escape` or `nano_attr_escape` before reaching HTML. -6. **Offline.** No external scripts, fonts, CSS, images, telemetry, or analytics. CSP defaults to `default-src 'none'`. Static mode forbids inline script. -7. **Interactive mode is copy-only.** Reserved for PR 4. PR 1 rejects `--interactive` with exit 2. +6. **Offline.** No external scripts, fonts, CSS, images, telemetry, or analytics. Static-mode CSP defaults to `default-src 'none'`. The interactive mode (see below) widens the policy to allow ONE inline script body, with strict allowlist enforcement. +7. **Interactive mode is copy-only.** When `--interactive` is passed for `/plan` or `/review`, the page emits three clipboard buttons (copy as prompt / Markdown / JSON patch). The inline script may use only `navigator.clipboard.writeText`. No `fetch`, no `XMLHttpRequest`, no `localStorage`, no `sessionStorage`, no `document.cookie`, no `eval`, no `new Function`, no `
`, no `document.write`, no `window.open`. Other phases reject `--interactive` with exit 1. A consumer that depends on any of these invariants can read the manifest (see below) to confirm the render produced today's contract. ## CLI ``` -bin/render-artifact.sh [artifact-path|--latest] [--strict] [--interactive] [--out ] [--manifest-only] +bin/render-artifact.sh [artifact-path|--latest] [--strict] + [--interactive] [--out ] + [--manifest-only] + +bin/render-artifact.sh journal [--today|--date YYYY-MM-DD] + +bin/render-artifact.sh stack [] [--strict] [--manifest-only] ``` | Form | Behavior | |------|----------| -| ` --latest` | Resolve source via `bin/find-artifact.sh 30`. | +| ` --latest` | Resolve source via `bin/find-artifact.sh 30 --no-session-sync`. | | ` ` | Render the explicit file. Phase mismatch fails with exit 1. | | `` with no artifact argument | Equivalent to ` --latest`. | -| `journal --today` | Reserved for PR 3. Exit 2 in PR 1. | -| `stack ` | Reserved for PR 3. Exit 2 in PR 1. | +| `journal --today` | Aggregate every registered phase artifact for today's date into a sprint timeline. | +| `journal --date YYYY-MM-DD` | Same shape, restricted to the requested date. Filename prefix filter; no 30-day fallback. | +| `journal` (no flag) | Defaults to today's UTC date. | +| `stack ` | Render a custom workflow DAG. Looks up `examples/custom-stack-template//stack.json`, then `$NANOSTACK_STORE/stacks//stack.json`. | +| `stack default` | Falls back to `.nanostack/config.json`'s `phase_graph` when no named stack file is found. Any other unknown name renders a "Stack not found" notice rather than falling back. | | Flag | Behavior | |------|----------| -| `--latest` | Resolve source with `find-artifact.sh`. | -| `--strict` | Require `nano_artifact_trust == verified`. | -| `--interactive` | Reserved for PR 4. Exit 2 in PR 1. | -| `--out ` | Write HTML to explicit path. Path must be inside `$NANOSTACK_STORE/visual/`. | -| `--manifest-only` | Write manifest only. Useful for CI trust checks. | +| `--latest` | Resolve source with `find-artifact.sh --no-session-sync`. | +| `--strict` | Require `nano_artifact_trust == verified` for the source artifact (phase renders) or for every aggregated source (journal / stack). Aggregate strict mode allows `missing` (a phase not run yet) but rejects `integrity_missing` and `integrity_mismatch`. | +| `--interactive` | Enable copy-only clipboard buttons. Supported only for `/plan` and `/review`. Exit 1 elsewhere. | +| `--out ` | Write HTML to explicit path. Path must lexically normalize under `$NANOSTACK_STORE/visual/` and may not traverse a symlinked directory. The leaf file may not pre-exist as a symlink or directory. | +| `--manifest-only` | Write manifest only. The strict check still runs first, so a malformed source still produces exit 3. | | Exit | Meaning | |------|---------| | 0 | Render succeeded. Output path printed to stdout. | -| 1 | Input error: missing artifact, invalid JSON, phase mismatch, unsupported phase. | -| 2 | Feature intentionally unsupported in current PR. | +| 1 | Input error: missing artifact, invalid JSON, phase mismatch, unsupported phase, malformed stack name, `--interactive` requested outside `/plan` and `/review`. | | 3 | Trust failure (`integrity_mismatch` always; `integrity_missing` only under `--strict`). | -| 4 | Unsafe output path or symlinked visual root. | +| 4 | Unsafe output path: outside the visual root, symlinked subdirectory, symlinked leaf, directory at leaf, or symlinked visual root. | ## Store layout ``` $NANOSTACK_STORE/visual/ / core phase HTML (plan, think, review, security, qa, ship) - custom// custom phase HTML + custom// custom phase HTML (future, when wired) journal/ sprint journal HTML + stack// custom stack DAG HTML manifests/ companion manifest JSON for every render ``` Filename format: -- HTML: `YYYYMMDD-HHMMSS-.html` -- Core manifest: `YYYYMMDD-HHMMSS-.manifest.json` -- Custom manifest: `YYYYMMDD-HHMMSS-custom-.manifest.json` +- Phase HTML: `YYYYMMDD-HHMMSS--.html` +- Journal HTML: `YYYYMMDD-HHMMSS--journal-.html` +- Stack HTML: `YYYYMMDD-HHMMSS--stack-.html` +- Manifest: matching stem with `.manifest.json` suffix, under `manifests/` + +The PID suffix prevents collisions between same-second renders. ## Manifest schema @@ -71,8 +83,8 @@ Every render writes a companion manifest. Schema version `1`: ```json { "schema_version": "1", - "kind": "phase", - "phase": "plan", + "kind": "phase|journal|stack", + "phase": "plan|think|review|security|qa|ship|journal|stack", "custom_phase": false, "format": "html", "interactive": false, @@ -85,11 +97,13 @@ Every render writes a companion manifest. Schema version `1`: "trust": "verified" } ], - "output_path": "/absolute/path/.nanostack/visual/plan/20260511-180100-plan.html", + "output_path": "/absolute/path/.nanostack/visual/plan/20260511-180100-12345-plan.html", "renderer": { "name": "nanostack-html-renderer", "version": "1" }, + "schema_valid": true, + "schema_error": null, "created_at": "2026-05-11T18:01:00Z" } ``` @@ -99,9 +113,12 @@ Validation rules: - `schema_version` equals `"1"`. - `format` equals `"html"`. - `kind` is one of `phase`, `journal`, `stack`. -- `source_artifacts` length is at least 1; each entry has `phase`, `path`, `trust`. +- `source_artifacts` length is at least 1. Phase renders have one entry; journal aggregates every registered phase; stack renders include the stack definition file (or `.nanostack/config.json` for `stack default`) as the first source, plus one entry per phase. - `output_path` is absolute and under `$NANOSTACK_STORE/visual/`. - `renderer.version` is present. +- `interactive` is `true` only for `/plan` and `/review` renders with `--interactive`. + +Even failure modes ("Stack invalid", "Stack not found") produce a manifest with a synthetic `stack:` source so downstream consumers always see at least one entry. ## HTML safety contract @@ -110,7 +127,8 @@ Every generated HTML document includes: - ``. - ``. - ``. -- A static-mode CSP: `default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'`. +- Static-mode CSP: `default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'`. +- Interactive-mode CSP: same as static plus `script-src 'unsafe-inline'`. - A `` of the form `Nanostack /<phase> visual artifact`. - A `<main>` element with `data-nanostack-visual="1"` and `data-phase="<phase>"`. - A trust badge element with `data-trust="<status>"`. @@ -118,12 +136,13 @@ Every generated HTML document includes: Forbidden in any generated template or rendered output: -- External URLs (`http://`, `https://`). +- External URLs (`http://`, `https://`) except inside the locked `nano_visual_safe_pr_url` allowlist for `/ship` PR links and the SVG XML namespace identifier. - `<script src=...>`, `fetch(`, `XMLHttpRequest`, `navigator.sendBeacon`. - `localStorage`, `sessionStorage`, `document.cookie`. - `eval(`, `new Function(`. +- `<form>`, `document.write`, `window.open`. -CI enforces the forbidden list against `bin/render-artifact.sh` and `bin/lib/visual-render.sh` via `ci/check-visual-artifact-templates.sh`. +The interactive inline script body is allowed to use `navigator.clipboard.writeText` only. CI enforces the forbidden list against `bin/render-artifact.sh`, `bin/lib/visual-render.sh`, and `bin/lib/html-escape.sh` via `ci/check-visual-artifact-templates.sh`. ## Escape helpers @@ -135,6 +154,8 @@ CI enforces the forbidden list against `bin/render-artifact.sh` and `bin/lib/vis Every JSON-derived string MUST pass through one of these. Fixed template HTML (page shell, table headers, button labels) may be written directly. +For interactive mode, an additional transform in `render-artifact.sh::_js_safe_for_script` encodes every `<` in JS-embedded JSON payloads as `<` so the HTML parser cannot exit the script body via `<!--<script>`, `</script>`, or `<!CDATA[` sequences. `JSON.parse` reads `<` as the literal `<`, so the clipboard payload the user pastes is unchanged. + ## Trust badge wording | Status | Badge text | @@ -142,20 +163,46 @@ Every JSON-derived string MUST pass through one of these. Fixed template HTML (p | `verified` | `verified` | | `integrity_missing` | `unverified` | | `integrity_mismatch` | This status never reaches the badge: the render exits 3 before HTML is written. | +| `not_applicable` | `aggregated` (used by journal and stack views, which surface per-source trust inline). | | `not_found` | This status never reaches the badge: the render exits 1 before HTML is written. | The wording is locked. A renderer that prints other strings for these statuses fails `ci/check-visual-artifact-templates.sh`. +## Multi-store trust scope + +In a project-local store (`$(git rev-parse --show-toplevel)/.nanostack`), the journal and stack renderers surface tampered same-day artifacts even when `.project` was flipped. The integrity hash is the authoritative signal because no one else writes to the project repo's `.nanostack/` directory. + +In a shared store (for example `$HOME/.nanostack` used across projects), the renderers require `.project` to match the current project before surfacing tamper. This avoids false positives from other projects' tampered or legacy artifacts. + +The store-local check uses `realpath` so a macOS `/tmp` → `/private/tmp` symlink does not produce a false mismatch. + +## Stack graph validation + +`stack` renders accept a `phase_graph` only if all of the following hold: + +- It is a non-empty array of objects. +- Each entry has a non-empty string `.name` and an array-of-string `.depends_on`. +- Names match `^[A-Za-z0-9_-]+$` (no whitespace, no path separators). +- Names are unique. +- Every name in any `.depends_on` exists as a declared node (no dangling references). +- The graph is acyclic (Kahn-style topological pass; rounds capped at `node_count + 1`). + +A graph that fails any check produces a "Stack invalid" notice with the specific reason. The manifest still writes with a synthetic source pointing at the stack file. + ## Relationship to existing primitives - Trust state comes from `bin/lib/artifact-trust.sh::nano_artifact_trust`. The renderer never reimplements integrity checks. -- Source artifact resolution uses `bin/find-artifact.sh` for `--latest` and accepts an explicit path otherwise. The renderer parses the explicit path with `jq -e .` and verifies `.phase` matches the requested phase. -- Schema validation uses `bin/lib/artifact-schemas.sh::nano_validate_artifact`. A source artifact that fails schema validation is rendered with a visible "schema invalid" notice rather than failing the render; the manifest still records the source trust. +- Source artifact resolution for phase renders uses `bin/find-artifact.sh --no-session-sync` so the renderer never mutates `session.json`. +- Journal date filtering uses a filename prefix sort (matches `save-artifact.sh`'s `YYYYMMDD-HHMMSS.json` convention), not file mtime. +- Schema validation uses `bin/lib/artifact-schemas.sh::nano_validate_artifact`. A source artifact that fails schema validation is rendered with a visible "schema invalid" notice rather than failing the render; the manifest records the source trust and an HTML data-testid marks the warning. - Store path comes from `bin/lib/store-path.sh::NANOSTACK_STORE`. +- Phase registry comes from `bin/lib/phases.sh::nano_all_phases` and `nano_phase_graph_json`. Custom phases declared in `.nanostack/config.json` appear in the journal timeline and the `stack default` fallback. - No skill, conductor command, or guard hook reads the HTML output. The renderer is strictly downstream. ## Determinism and atomicity -- Writes go to `<path>.tmp.$$` and rename into place after the render and manifest both succeed. -- Temporary files are removed on trap. +- Writes go through `mktemp "$path.tmp.XXXXXX"` (O_EXCL) and rename into place after the render and manifest both succeed. +- Temporary files and the per-render scratch directory are removed on trap. - The renderer prints exactly one line to stdout on success: the absolute HTML output path. Errors go to stderr. +- Same-second renders never collide on manifest paths because the timestamp includes the PID. +- Lexical path normalization (`nano_visual_normalize_path`) defeats `..` escape and symlink-then-up attacks before any filesystem write.