create-egregore 0.21.1 → 0.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/assets/egregore-launcher.js +2 -2
  2. package/lib/renderer-install.js +42 -0
  3. package/lib/setup.js +15 -14
  4. package/package.json +2 -2
  5. package/runtime/claude/.claude/skills/view/SKILL.md +2 -1
  6. package/runtime/claude/bin/artifact-renderer-install.sh +157 -0
  7. package/runtime/claude/bin/artifact-renderer.sh +57 -0
  8. package/runtime/claude/bin/handoff-preview.sh +2 -4
  9. package/runtime/claude/bin/handoff-run.sh +4 -11
  10. package/runtime/claude/bin/lib/artifact-renderer/dependencies-lock.json +94 -0
  11. package/runtime/claude/bin/lib/artifact-renderer/dependencies.json +8 -0
  12. package/runtime/claude/bin/lib/dashboard-artifact.sh +2 -2
  13. package/runtime/claude/bin/lib/greeting.sh +2 -13
  14. package/runtime/claude/bin/post-update.sh +5 -0
  15. package/runtime/claude/bin/publish-artifact.sh +6 -27
  16. package/runtime/claude/bin/view-render.sh +2 -13
  17. package/runtime/claude/egregore_runtime/upgrade.py +51 -0
  18. package/runtime/claude/manifest.json +4 -0
  19. package/runtime/codex/bin/artifact-renderer-install.sh +157 -0
  20. package/runtime/codex/bin/artifact-renderer.sh +57 -0
  21. package/runtime/codex/bin/handoff-preview.sh +2 -4
  22. package/runtime/codex/bin/handoff-run.sh +4 -11
  23. package/runtime/codex/bin/lib/artifact-renderer/dependencies-lock.json +94 -0
  24. package/runtime/codex/bin/lib/artifact-renderer/dependencies.json +8 -0
  25. package/runtime/codex/bin/lib/dashboard-artifact.sh +2 -2
  26. package/runtime/codex/bin/lib/greeting.sh +2 -13
  27. package/runtime/codex/bin/post-update.sh +5 -0
  28. package/runtime/codex/bin/publish-artifact.sh +6 -27
  29. package/runtime/codex/bin/view-render.sh +2 -13
  30. package/runtime/codex/egregore_runtime/upgrade.py +51 -0
  31. package/runtime/codex/manifest.json +4 -0
  32. package/runtime/pi/bin/artifact-renderer-install.sh +157 -0
  33. package/runtime/pi/bin/artifact-renderer.sh +57 -0
  34. package/runtime/pi/bin/handoff-preview.sh +2 -4
  35. package/runtime/pi/bin/handoff-run.sh +4 -11
  36. package/runtime/pi/bin/lib/artifact-renderer/dependencies-lock.json +94 -0
  37. package/runtime/pi/bin/lib/artifact-renderer/dependencies.json +8 -0
  38. package/runtime/pi/bin/lib/dashboard-artifact.sh +2 -2
  39. package/runtime/pi/bin/lib/greeting.sh +2 -13
  40. package/runtime/pi/bin/pi-product-workflows.mjs +1 -4
  41. package/runtime/pi/bin/post-update.sh +5 -0
  42. package/runtime/pi/bin/publish-artifact.sh +6 -27
  43. package/runtime/pi/bin/view-render.sh +2 -13
  44. package/runtime/pi/egregore_runtime/upgrade.py +51 -0
  45. package/runtime/pi/manifest.json +4 -0
  46. package/runtime/prime/bin/artifact-renderer-install.sh +157 -0
  47. package/runtime/prime/bin/artifact-renderer.sh +57 -0
  48. package/runtime/prime/bin/handoff-preview.sh +2 -4
  49. package/runtime/prime/bin/handoff-run.sh +4 -11
  50. package/runtime/prime/bin/lib/artifact-renderer/dependencies-lock.json +94 -0
  51. package/runtime/prime/bin/lib/artifact-renderer/dependencies.json +8 -0
  52. package/runtime/prime/bin/lib/dashboard-artifact.sh +2 -2
  53. package/runtime/prime/bin/lib/greeting.sh +2 -13
  54. package/runtime/prime/bin/pi-product-workflows.mjs +1 -4
  55. package/runtime/prime/bin/post-update.sh +5 -0
  56. package/runtime/prime/bin/publish-artifact.sh +6 -27
  57. package/runtime/prime/bin/view-render.sh +2 -13
  58. package/runtime/prime/egregore_runtime/upgrade.py +51 -0
  59. package/runtime/prime/manifest.json +4 -0
@@ -17,6 +17,7 @@ from __future__ import annotations
17
17
  import json
18
18
  import os
19
19
  import re
20
+ import signal
20
21
  import subprocess
21
22
  import sys
22
23
  import time
@@ -27,6 +28,8 @@ from typing import Any, Mapping
27
28
  from .adapters.qmd import QMD_VERSION, RERANKING_ENABLED, QmdLocalRetriever
28
29
 
29
30
  SCHEMA_VERSION = "egregore-runtime-upgrade/v1"
31
+ _RENDERER_INSTALL_TIMEOUT_SECONDS = 180
32
+ _RENDERER_TERMINATE_GRACE_SECONDS = 5
30
33
 
31
34
  # Stage keys in preparation order. Labels are the user-facing truth shown by
32
35
  # Settings; keep them in plain team language.
@@ -628,6 +631,54 @@ class UpgradeEngine:
628
631
  if probe.returncode != 0:
629
632
  self._fail(state, "node", "the selected Node runtime failed its version probe")
630
633
  return False
634
+ # Explicit Runtime preparation owns package provisioning. Preview and
635
+ # session startup only resolve installed renderers. Older candidates
636
+ # predate this helper and retain their original upgrade contract.
637
+ renderer_installer = (
638
+ Path(state.get("installed_path") or "")
639
+ / "runtime" / APPLY_TREE / "bin" / "artifact-renderer-install.sh"
640
+ )
641
+ if renderer_installer.is_file():
642
+ environment = {**os.environ, **self.retriever.environment}
643
+ environment["PATH"] = str(Path(node).parent) + os.pathsep + environment.get("PATH", "")
644
+ try:
645
+ with subprocess.Popen(
646
+ ("bash", str(renderer_installer), "--root", str(self.repository_root)),
647
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
648
+ cwd=self.repository_root, env=environment, start_new_session=True,
649
+ ) as renderer:
650
+ try:
651
+ stdout, stderr = renderer.communicate(timeout=_RENDERER_INSTALL_TIMEOUT_SECONDS)
652
+ except subprocess.TimeoutExpired:
653
+ # npm is a child of the installer shell. Stop the entire
654
+ # group so its EXIT cleanup can run without an orphaned
655
+ # package manager still writing into the staging tree.
656
+ try:
657
+ os.killpg(renderer.pid, signal.SIGTERM)
658
+ except ProcessLookupError:
659
+ pass
660
+ try:
661
+ renderer.communicate(timeout=_RENDERER_TERMINATE_GRACE_SECONDS)
662
+ except subprocess.TimeoutExpired:
663
+ pass
664
+ finally:
665
+ # A child may ignore TERM even after Bash exits.
666
+ try:
667
+ os.killpg(renderer.pid, signal.SIGKILL)
668
+ except ProcessLookupError:
669
+ pass
670
+ renderer.communicate()
671
+ raise
672
+ except subprocess.TimeoutExpired:
673
+ self._fail(state, "node", f"artifact renderer provisioning timed out after {_RENDERER_INSTALL_TIMEOUT_SECONDS} seconds; retry preparation")
674
+ return False
675
+ except OSError as exc:
676
+ self._fail(state, "node", f"artifact renderer provisioning failed: {exc}")
677
+ return False
678
+ if renderer.returncode != 0:
679
+ detail = (stderr or stdout).strip()
680
+ self._fail(state, "node", f"artifact renderer provisioning failed: {detail}")
681
+ return False
631
682
  self._mark(state, "node", "done", f"{probe.stdout.strip()} at instance-selected path")
632
683
  return True
633
684
 
@@ -135,6 +135,8 @@
135
135
  "bin/agent.sh",
136
136
  "bin/api-call.sh",
137
137
  "bin/artifact-register.sh",
138
+ "bin/artifact-renderer-install.sh",
139
+ "bin/artifact-renderer.sh",
138
140
  "bin/artifact-writeback.sh",
139
141
  "bin/artifacts.sh",
140
142
  "bin/attendant.sh",
@@ -182,6 +184,8 @@
182
184
  "bin/issue.sh",
183
185
  "bin/knowledge.sh",
184
186
  "bin/lib/artifact-id.sh",
187
+ "bin/lib/artifact-renderer/dependencies-lock.json",
188
+ "bin/lib/artifact-renderer/dependencies.json",
185
189
  "bin/lib/config.sh",
186
190
  "bin/lib/context.sh",
187
191
  "bin/lib/dashboard-artifact.sh",
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env bash
2
+ # Explicit setup/update/repair only. Rendering and startup must never install.
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
6
+ TARGET_ROOT="$SCRIPT_DIR"
7
+ CHECK_ONLY=0
8
+ LOCK_FD=""
9
+ while [ "$#" -gt 0 ]; do
10
+ case "$1" in
11
+ --root)
12
+ [ "$#" -ge 2 ] && [ -n "$2" ] || { echo 'artifact renderer: --root needs a directory' >&2; exit 2; }
13
+ TARGET_ROOT="$2"; shift 2 ;;
14
+ --check) CHECK_ONLY=1; shift ;;
15
+ --lock-fd) LOCK_FD="${2:?missing lock descriptor}"; shift 2 ;;
16
+ *) echo 'Usage: bash bin/artifact-renderer-install.sh [--root <instance>] [--check]' >&2; exit 2 ;;
17
+ esac
18
+ done
19
+ # npm 11 treats a symlinked --prefix as a linked root package, which makes
20
+ # `ci` reject the lockfile (including macOS's /var -> /private/var alias).
21
+ TARGET_ROOT="$(cd "$TARGET_ROOT" && pwd -P)"
22
+ MANIFEST_DIR="$SCRIPT_DIR/bin/lib/artifact-renderer"
23
+
24
+ for required in dependencies.json dependencies-lock.json; do
25
+ [ -f "$MANIFEST_DIR/$required" ] || {
26
+ echo "artifact renderer: pinned $required is missing; update the framework and retry" >&2
27
+ exit 1
28
+ }
29
+ done
30
+ NODE_BIN="$(bash "$SCRIPT_DIR/bin/node-run.sh" -p 'process.execPath')"
31
+ export PATH="$(dirname "$NODE_BIN"):$PATH"
32
+ MANIFEST_INFO="$(python3 - "$MANIFEST_DIR" <<'PY'
33
+ import hashlib, json, pathlib, re, sys
34
+ source = pathlib.Path(sys.argv[1])
35
+ version = json.loads((source / 'dependencies.json').read_text())['dependencies']['egregore-artifacts']
36
+ if not isinstance(version, str) or not re.fullmatch(r'[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?', version):
37
+ raise SystemExit('artifact renderer: an exact package version is required')
38
+ digest = hashlib.sha256((source / 'dependencies-lock.json').read_bytes()).hexdigest()[:16]
39
+ print(version, version + '-' + digest)
40
+ PY
41
+ )"
42
+ read -r VERSION INSTALL_ID <<< "$MANIFEST_INFO"
43
+ TOOLS_DIR="$TARGET_ROOT/.egregore/tools/artifact-renderer"
44
+ PREFIX="$TOOLS_DIR/$INSTALL_ID"
45
+
46
+ renderer_ready() {
47
+ local candidate="$1"
48
+ cmp -s "$MANIFEST_DIR/dependencies.json" "$candidate/package.json" || return 1
49
+ cmp -s "$MANIFEST_DIR/dependencies-lock.json" "$candidate/package-lock.json" || return 1
50
+ "$NODE_BIN" -e '
51
+ const fs = require("node:fs");
52
+ const path = require("node:path");
53
+ const actual = JSON.parse(fs.readFileSync(path.join(process.argv[1], "node_modules/egregore-artifacts/package.json"), "utf8"));
54
+ if (actual.version !== process.argv[2]) process.exit(1);
55
+ ' "$candidate" "$VERSION" >/dev/null 2>&1 || return 1
56
+ "$NODE_BIN" "$candidate/node_modules/egregore-artifacts/bin/cli.js" --help >/dev/null 2>&1
57
+ }
58
+
59
+ if renderer_ready "$PREFIX"; then
60
+ printf 'Artifact renderer %s is ready\n' "$VERSION"
61
+ exit 0
62
+ fi
63
+ if [ "$CHECK_ONLY" = 1 ]; then
64
+ printf 'Artifact renderer %s is not ready; run bash bin/artifact-renderer-install.sh\n' "$VERSION" >&2
65
+ exit 1
66
+ fi
67
+
68
+ NPM_BIN="$(command -v npm || true)"
69
+ [ -n "$NPM_BIN" ] && "$NPM_BIN" --version >/dev/null 2>&1 || {
70
+ echo 'artifact renderer: npm is unavailable; install Node with npm, then retry' >&2
71
+ exit 1
72
+ }
73
+ mkdir -p "$TOOLS_DIR"
74
+ [ ! -L "$PREFIX" ] || { echo 'artifact renderer: managed install must not be a symbolic link' >&2; exit 1; }
75
+ LOCK="$TOOLS_DIR/.$INSTALL_ID.lock"
76
+ # A kernel lock survives in children while they are still installing and is
77
+ # released after forced interruption. Its persistent file never means "busy".
78
+ # Re-exec under the inherited descriptor so Bash, npm, and cleanup all share
79
+ # ownership; unlike mkdir locks, a killed process cannot leave a stale owner.
80
+ if [ -z "$LOCK_FD" ]; then
81
+ exec python3 - "$LOCK" "$SCRIPT_DIR/bin/artifact-renderer-install.sh" "$TARGET_ROOT" <<'PY'
82
+ import fcntl, os, stat, sys
83
+ lock, script, root = sys.argv[1:]
84
+ fd = os.open(lock, os.O_CREAT | os.O_RDWR | getattr(os, 'O_NOFOLLOW', 0), 0o600)
85
+ if not stat.S_ISREG(os.fstat(fd).st_mode):
86
+ raise SystemExit('artifact renderer: invalid install lock')
87
+ try:
88
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
89
+ except BlockingIOError:
90
+ raise SystemExit('artifact renderer: another install is in progress; retry after it finishes')
91
+ os.set_inheritable(fd, True)
92
+ os.execvp('bash', ['bash', script, '--root', root, '--lock-fd', str(fd)])
93
+ PY
94
+ fi
95
+ # Validate the internal descriptor instead of accepting an unlocked re-entry.
96
+ python3 - "$LOCK_FD" "$LOCK" <<'PY'
97
+ import fcntl, os, sys
98
+ try:
99
+ fd = int(sys.argv[1])
100
+ if not os.path.samestat(os.fstat(fd), os.stat(sys.argv[2])):
101
+ raise ValueError('wrong lock file')
102
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
103
+ except (ValueError, OSError):
104
+ raise SystemExit('artifact renderer: install lock is unavailable')
105
+ PY
106
+ STAGE=""
107
+ BACKUP="$TOOLS_DIR/.$INSTALL_ID.previous"
108
+ cleanup() {
109
+ if [ -n "$BACKUP" ] && [ -d "$BACKUP" ] && [ ! -e "$PREFIX" ]; then mv "$BACKUP" "$PREFIX"; fi
110
+ [ -z "$STAGE" ] || rm -rf "$STAGE"
111
+ }
112
+ trap cleanup EXIT
113
+ trap 'exit 130' INT
114
+ trap 'exit 143' TERM
115
+
116
+ # Recover an interrupted swap and discard only this pin's abandoned staging
117
+ # trees. Another Runtime's version/lock and in-progress install stay untouched.
118
+ if [ -d "$BACKUP" ]; then
119
+ if [ ! -e "$PREFIX" ]; then mv "$BACKUP" "$PREFIX"; else rm -rf "$BACKUP"; fi
120
+ fi
121
+ for abandoned in "$TOOLS_DIR/.$INSTALL_ID.stage."*; do
122
+ [ ! -d "$abandoned" ] || rm -rf "$abandoned"
123
+ done
124
+
125
+ # Recheck under the lock in case another explicit installer just completed.
126
+ if renderer_ready "$PREFIX"; then
127
+ printf 'Artifact renderer %s is ready\n' "$VERSION"
128
+ exit 0
129
+ fi
130
+ STAGE="$(mktemp -d "$TOOLS_DIR/.$INSTALL_ID.stage.XXXXXX")"
131
+ cp "$MANIFEST_DIR/dependencies.json" "$STAGE/package.json"
132
+ cp "$MANIFEST_DIR/dependencies-lock.json" "$STAGE/package-lock.json"
133
+ printf 'Installing artifact renderer %s with locked dependencies…\n' "$VERSION"
134
+ if ! "$NPM_BIN" ci --prefix "$STAGE" --ignore-scripts --omit=dev --no-audit --no-fund >"$STAGE/install.log" 2>&1; then
135
+ echo 'artifact renderer: installation failed; the previous install was kept' >&2
136
+ # npm often appends a long usage guide after the actionable error.
137
+ head -12 "$STAGE/install.log" >&2
138
+ if [ "$(wc -l < "$STAGE/install.log")" -gt 20 ]; then
139
+ echo '… npm output abbreviated …' >&2
140
+ tail -8 "$STAGE/install.log" >&2
141
+ fi
142
+ exit 1
143
+ fi
144
+ if ! renderer_ready "$STAGE"; then
145
+ echo 'artifact renderer: the installed package failed its readiness check; the previous install was kept' >&2
146
+ exit 1
147
+ fi
148
+ rm -f "$STAGE/install.log"
149
+ # Both renames are on the same filesystem. No live install is removed until
150
+ # the complete replacement has passed its version/dependency probe.
151
+ if [ -e "$PREFIX" ]; then
152
+ mv "$PREFIX" "$BACKUP"
153
+ fi
154
+ mv "$STAGE" "$PREFIX"
155
+ STAGE=""
156
+ [ ! -d "$BACKUP" ] || rm -rf "$BACKUP"
157
+ printf 'Artifact renderer %s installed\n' "$VERSION"
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # One offline resolver for source checkouts and installed Egregore instances.
5
+ # Provisioning is explicit (setup/update/artifact-renderer-install.sh), never
6
+ # a side effect of rendering private content.
7
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
8
+
9
+ resolve_renderer() {
10
+ local source_cli="$ROOT/packages/egregore-artifacts/bin/cli.js"
11
+ local manifest_dir="$ROOT/bin/lib/artifact-renderer"
12
+ local managed="" install_id=""
13
+ local installed=""
14
+
15
+ if [ "${EGREGORE_USE_PUBLISHED:-0}" != "1" ] && \
16
+ [ -f "$source_cli" ] && \
17
+ [ -d "$ROOT/packages/egregore-artifacts/node_modules/react" ]; then
18
+ printf '%s\n' "$source_cli"
19
+ return 0
20
+ fi
21
+ if [ -f "$manifest_dir/dependencies.json" ] && [ -f "$manifest_dir/dependencies-lock.json" ]; then
22
+ install_id="$(python3 - "$manifest_dir" <<'PY'
23
+ import hashlib, json, pathlib, re, sys
24
+ source = pathlib.Path(sys.argv[1])
25
+ version = json.loads((source / 'dependencies.json').read_text())['dependencies']['egregore-artifacts']
26
+ if not isinstance(version, str) or not re.fullmatch(r'[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?', version):
27
+ raise SystemExit('artifact renderer: an exact package version is required')
28
+ digest = hashlib.sha256((source / 'dependencies-lock.json').read_bytes()).hexdigest()[:16]
29
+ print(version + '-' + digest)
30
+ PY
31
+ )" || return 1
32
+ managed="$ROOT/.egregore/tools/artifact-renderer/$install_id"
33
+ if cmp -s "$manifest_dir/dependencies.json" "$managed/package.json" && \
34
+ cmp -s "$manifest_dir/dependencies-lock.json" "$managed/package-lock.json" && \
35
+ [ -f "$managed/node_modules/egregore-artifacts/bin/cli.js" ] && \
36
+ [ -d "$managed/node_modules/react" ]; then
37
+ printf '%s\n' "$managed/node_modules/egregore-artifacts/bin/cli.js"
38
+ return 0
39
+ fi
40
+ fi
41
+ installed="$(command -v egregore-artifacts 2>/dev/null || true)"
42
+ if [ -n "$installed" ] && [ -f "$installed" ]; then
43
+ printf '%s\n' "$installed"
44
+ return 0
45
+ fi
46
+ echo "The artifact renderer is not installed or its dependencies are missing." >&2
47
+ echo "Run bash bin/artifact-renderer-install.sh from this instance, then retry." >&2
48
+ return 1
49
+ }
50
+
51
+ renderer="$(resolve_renderer)" || exit 1
52
+ if [ "${1:-}" = "--resolve" ]; then
53
+ [ "$#" -eq 1 ] || { echo "--resolve takes no arguments" >&2; exit 2; }
54
+ printf '%s\n' "$renderer"
55
+ exit 0
56
+ fi
57
+ exec bash "$ROOT/bin/node-run.sh" "$renderer" "$@"
@@ -110,13 +110,11 @@ preview() {
110
110
 
111
111
  local render_ok=0
112
112
  if [ -n "$composed" ]; then
113
- if bash "$ROOT/bin/node-run.sh" \
114
- "$ROOT/packages/egregore-artifacts/bin/cli.js" composed "$spec_file" \
113
+ if bash "$ROOT/bin/artifact-renderer.sh" composed "$spec_file" \
115
114
  --source "$content_file" --verify-fidelity; then
116
115
  render_ok=1
117
116
  fi
118
- elif bash "$ROOT/bin/node-run.sh" \
119
- "$ROOT/packages/egregore-artifacts/bin/cli.js" handoff "$content_file" \
117
+ elif bash "$ROOT/bin/artifact-renderer.sh" handoff "$content_file" \
120
118
  --verify-fidelity; then
121
119
  render_ok=1
122
120
  fi
@@ -414,21 +414,14 @@ PID_MEMORY=$!
414
414
  # keep their familiar look.
415
415
  RENDERED_HTML="$TMPD/handoff.html"
416
416
  RENDER_OK=0
417
- LOCAL_CLI="$SCRIPT_DIR/packages/egregore-artifacts/bin/cli.js"
418
417
  FIDELITY_ARGS=(--verify-fidelity)
419
418
  if [ "$RENDER_TYPE" = "composed" ]; then
420
419
  FIDELITY_ARGS+=(--source "$ABS_FILE")
421
420
  fi
422
- # Node always runs through the adapter (broken Volta/NVM shims are a
423
- # live hazard in harness shells), and there is no npx/@latest fallback:
424
- # a hidden registry fetch of a floating version is never an acceptable
425
- # renderer. Missing local renderer → structured-only handoff.
426
- if [ -f "$LOCAL_CLI" ] && [ -d "$SCRIPT_DIR/packages/egregore-artifacts/node_modules/react" ]; then
427
- if bash "$SCRIPT_DIR/bin/node-run.sh" "$LOCAL_CLI" "$RENDER_TYPE" "${RENDER_SRC:-$ABS_FILE}" "${FIDELITY_ARGS[@]}" --output "$RENDERED_HTML" >/dev/null 2>"$TMPD/render-error"; then
428
- RENDER_OK=1
429
- fi
430
- else
431
- echo "local egregore-artifacts renderer unavailable; sending structured handoff without custom HTML" > "$TMPD/render-error"
421
+ # Resolve exactly as previews do, without a registry fetch. An unavailable
422
+ # renderer still leaves the existing structured-only handoff fallback.
423
+ if bash "$SCRIPT_DIR/bin/artifact-renderer.sh" "$RENDER_TYPE" "${RENDER_SRC:-$ABS_FILE}" "${FIDELITY_ARGS[@]}" --output "$RENDERED_HTML" >/dev/null 2>"$TMPD/render-error"; then
424
+ RENDER_OK=1
432
425
  fi
433
426
 
434
427
  # Step 2: build the emissary payload. If rendering succeeded AND the
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "egregore-managed-artifact-renderer",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "egregore-managed-artifact-renderer",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "egregore-artifacts": "0.30.0"
12
+ }
13
+ },
14
+ "node_modules/@json-render/core": {
15
+ "version": "0.16.0",
16
+ "resolved": "https://registry.npmjs.org/@json-render/core/-/core-0.16.0.tgz",
17
+ "integrity": "sha512-qQp8BB/3pWYapTGXBDSBMXRCdrC05VJPLL3drXMPX/QbUB3nuvtyXUGmAZFUz8eLUy7JImODvb3GNIq38dGzhQ==",
18
+ "license": "Apache-2.0",
19
+ "dependencies": {
20
+ "zod": "^4.3.6"
21
+ },
22
+ "peerDependencies": {
23
+ "zod": "^4.0.0"
24
+ }
25
+ },
26
+ "node_modules/@json-render/react": {
27
+ "version": "0.16.0",
28
+ "resolved": "https://registry.npmjs.org/@json-render/react/-/react-0.16.0.tgz",
29
+ "integrity": "sha512-PJQjLftGYZ5LNlA1IFww+yNOAJZRW2feEcH/L10u/8M/b/zImr9wfqQVMDyC87myoEzhyucpkQ6joISeZdKxsw==",
30
+ "license": "Apache-2.0",
31
+ "dependencies": {
32
+ "@json-render/core": "0.16.0"
33
+ },
34
+ "peerDependencies": {
35
+ "react": "^19.2.3"
36
+ }
37
+ },
38
+ "node_modules/egregore-artifacts": {
39
+ "version": "0.30.0",
40
+ "resolved": "https://registry.npmjs.org/egregore-artifacts/-/egregore-artifacts-0.30.0.tgz",
41
+ "integrity": "sha512-0mihiLED2+n3CT4/VNBW7driwTdQwNaDztJ0T5/TFZ9qJhtUAJCyeawKfnOXgjuTkwwu+d6dY/KMuZMGONGDvQ==",
42
+ "license": "MIT",
43
+ "dependencies": {
44
+ "@json-render/core": "^0.16.0",
45
+ "@json-render/react": "^0.16.0",
46
+ "react": "^19.2.4",
47
+ "react-dom": "^19.2.4",
48
+ "zod": "^4.3.6"
49
+ },
50
+ "bin": {
51
+ "egregore-artifacts": "bin/cli.js"
52
+ },
53
+ "engines": {
54
+ "node": ">=18"
55
+ }
56
+ },
57
+ "node_modules/react": {
58
+ "version": "19.3.0",
59
+ "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
60
+ "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
61
+ "license": "MIT",
62
+ "engines": {
63
+ "node": ">=0.10.0"
64
+ }
65
+ },
66
+ "node_modules/react-dom": {
67
+ "version": "19.3.0",
68
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",
69
+ "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==",
70
+ "license": "MIT",
71
+ "dependencies": {
72
+ "scheduler": "^0.28.0"
73
+ },
74
+ "peerDependencies": {
75
+ "react": "^19.3.0"
76
+ }
77
+ },
78
+ "node_modules/scheduler": {
79
+ "version": "0.28.0",
80
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
81
+ "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
82
+ "license": "MIT"
83
+ },
84
+ "node_modules/zod": {
85
+ "version": "4.6.5",
86
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz",
87
+ "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==",
88
+ "license": "MIT",
89
+ "funding": {
90
+ "url": "https://github.com/sponsors/colinhacks"
91
+ }
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "egregore-managed-artifact-renderer",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "dependencies": {
6
+ "egregore-artifacts": "0.30.0"
7
+ }
8
+ }
@@ -121,7 +121,7 @@ mkdir -p /tmp
121
121
  } > "$_DB_MD" 2>/dev/null
122
122
 
123
123
  # --- Publish markdown → HTML → hosted URL ---
124
- # publish-artifact.sh handles HTML generation internally (npx egregore-artifacts)
124
+ # publish-artifact.sh handles HTML generation with the provisioned renderer.
125
125
  # then uploads. Don't pre-generate HTML — that causes double-wrapping.
126
126
  # https:// URLs are universally cmd+clickable across all terminal emulators.
127
127
  #
@@ -233,7 +233,7 @@ else
233
233
  if [ -z "$DASHBOARD_URL" ]; then
234
234
  _DB_HTML="/tmp/egregore-artifacts/dashboard-${EGREGORE_SESSION_ID}.html"
235
235
  mkdir -p /tmp/egregore-artifacts
236
- if npx egregore-artifacts@latest document "$_DB_MD" --output "$_DB_HTML" --no-open >/dev/null 2>&1 && [ -f "$_DB_HTML" ]; then
236
+ if bash "$SCRIPT_DIR/bin/artifact-renderer.sh" document "$_DB_MD" --output "$_DB_HTML" --no-open >/dev/null 2>&1 && [ -f "$_DB_HTML" ]; then
237
237
  DASHBOARD_URL="file://${_DB_HTML}"
238
238
  fi
239
239
  fi
@@ -481,19 +481,8 @@ bash "$SCRIPT_DIR/bin/telemetry.sh" emit "session_start" \
481
481
  # --- Health check-in (background, non-blocking) ---
482
482
  bash "$SCRIPT_DIR/bin/startup-check.sh" >/dev/null 2>&1 &
483
483
 
484
- # --- Warm npx cache for egregore-artifacts (background, throttled) ---
485
- # publish-artifact.sh spawns `npx egregore-artifacts` — first run in a
486
- # fresh npx cache pays a 5-10s npm download. Warmup is throttled to
487
- # once per hour via a marker file so we don't storm npm when a user
488
- # opens many tabs, and don't start runaway retry loops when offline.
489
- _WARM_TS_FILE="$HOME/.cache/egregore/npx-warm-ts"
490
- mkdir -p "$(dirname "$_WARM_TS_FILE")" 2>/dev/null
491
- _WARM_LAST=$(cat "$_WARM_TS_FILE" 2>/dev/null || echo 0)
492
- _WARM_NOW=$(date +%s)
493
- if [ $(( _WARM_NOW - _WARM_LAST )) -gt 3600 ]; then
494
- (npx -y egregore-artifacts@latest --help >/dev/null 2>&1 \
495
- && echo "$_WARM_NOW" > "$_WARM_TS_FILE" &) >/dev/null 2>&1
496
- fi
484
+ # Artifact dependencies are provisioned during explicit setup/update. Startup
485
+ # must not warm a floating npx cache that the offline renderer cannot resolve.
497
486
 
498
487
  # Clean up temp files
499
488
  rm -rf "$CTX_DIR"
@@ -10,6 +10,11 @@
10
10
 
11
11
  set -euo pipefail
12
12
 
13
+ # This migration entry point is invoked by explicit /update, never startup.
14
+ # The helper preserves a previous working install if provisioning fails.
15
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
16
+ bash "$SCRIPT_DIR/bin/artifact-renderer-install.sh"
17
+
13
18
  MIGRATED=0
14
19
 
15
20
  # --- Migration: commands → skills (CC 2.0+) ---
@@ -242,34 +242,13 @@ else
242
242
  fi
243
243
  fi
244
244
 
245
- # Prefer the in-repo package copy when it's present AND its dependencies
246
- # have been installed — that way local edits to packages/egregore-artifacts
247
- # are exercised without waiting for an npm release. Fall back to the
248
- # published `npx egregore-artifacts` otherwise.
249
- # Set EGREGORE_USE_PUBLISHED=1 to force the npm-published renderer even
250
- # when a local install is present — useful in the upstream dev repo to
251
- # dogfood what teammates and OSS users actually run.
252
- LOCAL_CLI="$SCRIPT_DIR/packages/egregore-artifacts/bin/cli.js"
245
+ # Use the same provisioned renderer as previews and /view. Rendering never
246
+ # downloads a package; EGREGORE_USE_PUBLISHED skips the source checkout.
253
247
  RENDER_ERROR="/tmp/egregore-artifacts/render-error-$$.log"
254
- if [ "${EGREGORE_USE_PUBLISHED:-0}" != "1" ] && [ -f "$LOCAL_CLI" ] && [ -d "$SCRIPT_DIR/packages/egregore-artifacts/node_modules/react" ]; then
255
- if ! bash "$SCRIPT_DIR/bin/node-run.sh" "$LOCAL_CLI" "${RENDER_ARGS[@]}" >/dev/null 2>"$RENDER_ERROR"; then
256
- sed -n '1,4p' "$RENDER_ERROR" >&2
257
- rm -f "$RENDER_ERROR"
258
- exit 5
259
- fi
260
- else
261
- # Installed published renderer only — never npx/@latest: a hidden
262
- # registry fetch of a floating version is not an acceptable renderer.
263
- PUBLISHED_CLI="$(command -v egregore-artifacts || true)"
264
- if [ -z "$PUBLISHED_CLI" ]; then
265
- echo "no artifact renderer available: the checked-out packages/egregore-artifacts is unusable and no installed egregore-artifacts was found" >&2
266
- exit 5
267
- fi
268
- if ! bash "$SCRIPT_DIR/bin/node-run.sh" "$PUBLISHED_CLI" "${RENDER_ARGS[@]}" >/dev/null 2>"$RENDER_ERROR"; then
269
- sed -n '1,4p' "$RENDER_ERROR" >&2
270
- rm -f "$RENDER_ERROR"
271
- exit 5
272
- fi
248
+ if ! bash "$SCRIPT_DIR/bin/artifact-renderer.sh" "${RENDER_ARGS[@]}" >/dev/null 2>"$RENDER_ERROR"; then
249
+ sed -n '1,4p' "$RENDER_ERROR" >&2
250
+ rm -f "$RENDER_ERROR"
251
+ exit 5
273
252
  fi
274
253
  rm -f "$RENDER_ERROR"
275
254
  fi
@@ -49,19 +49,8 @@ _open() {
49
49
  }
50
50
 
51
51
  # --- Resolve the packaged deterministic renderer (never fetch, never npx) ---
52
- if [ -f "$SCRIPT_DIR/packages/egregore-artifacts/bin/cli.js" ] && [ -d "$SCRIPT_DIR/packages/egregore-artifacts/node_modules/react" ]; then
53
- RENDER=(bash "$SCRIPT_DIR/bin/node-run.sh" "$SCRIPT_DIR/packages/egregore-artifacts/bin/cli.js")
54
- RENDER_ID="$SCRIPT_DIR/packages/egregore-artifacts/bin/cli.js"
55
- elif [ -f "$SCRIPT_DIR/packages/egregore-artifacts/bin/cli.js" ]; then
56
- echo "The checked-out artifact renderer is missing its local dependencies." >&2
57
- exit 1
58
- elif command -v egregore-artifacts >/dev/null 2>&1; then
59
- RENDER=(bash "$SCRIPT_DIR/bin/node-run.sh" "$(command -v egregore-artifacts)")
60
- RENDER_ID="$(command -v egregore-artifacts)"
61
- else
62
- echo "The local artifact renderer is not installed for this instance." >&2
63
- exit 1
64
- fi
52
+ RENDER_ID="$(bash "$SCRIPT_DIR/bin/artifact-renderer.sh" --resolve)" || exit 1
53
+ RENDER=(bash "$SCRIPT_DIR/bin/node-run.sh" "$RENDER_ID")
65
54
 
66
55
  # --- Live surfaces render fresh every time (their data is the present) ------
67
56
  case "$TYPE" in
@@ -17,6 +17,7 @@ from __future__ import annotations
17
17
  import json
18
18
  import os
19
19
  import re
20
+ import signal
20
21
  import subprocess
21
22
  import sys
22
23
  import time
@@ -27,6 +28,8 @@ from typing import Any, Mapping
27
28
  from .adapters.qmd import QMD_VERSION, RERANKING_ENABLED, QmdLocalRetriever
28
29
 
29
30
  SCHEMA_VERSION = "egregore-runtime-upgrade/v1"
31
+ _RENDERER_INSTALL_TIMEOUT_SECONDS = 180
32
+ _RENDERER_TERMINATE_GRACE_SECONDS = 5
30
33
 
31
34
  # Stage keys in preparation order. Labels are the user-facing truth shown by
32
35
  # Settings; keep them in plain team language.
@@ -628,6 +631,54 @@ class UpgradeEngine:
628
631
  if probe.returncode != 0:
629
632
  self._fail(state, "node", "the selected Node runtime failed its version probe")
630
633
  return False
634
+ # Explicit Runtime preparation owns package provisioning. Preview and
635
+ # session startup only resolve installed renderers. Older candidates
636
+ # predate this helper and retain their original upgrade contract.
637
+ renderer_installer = (
638
+ Path(state.get("installed_path") or "")
639
+ / "runtime" / APPLY_TREE / "bin" / "artifact-renderer-install.sh"
640
+ )
641
+ if renderer_installer.is_file():
642
+ environment = {**os.environ, **self.retriever.environment}
643
+ environment["PATH"] = str(Path(node).parent) + os.pathsep + environment.get("PATH", "")
644
+ try:
645
+ with subprocess.Popen(
646
+ ("bash", str(renderer_installer), "--root", str(self.repository_root)),
647
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
648
+ cwd=self.repository_root, env=environment, start_new_session=True,
649
+ ) as renderer:
650
+ try:
651
+ stdout, stderr = renderer.communicate(timeout=_RENDERER_INSTALL_TIMEOUT_SECONDS)
652
+ except subprocess.TimeoutExpired:
653
+ # npm is a child of the installer shell. Stop the entire
654
+ # group so its EXIT cleanup can run without an orphaned
655
+ # package manager still writing into the staging tree.
656
+ try:
657
+ os.killpg(renderer.pid, signal.SIGTERM)
658
+ except ProcessLookupError:
659
+ pass
660
+ try:
661
+ renderer.communicate(timeout=_RENDERER_TERMINATE_GRACE_SECONDS)
662
+ except subprocess.TimeoutExpired:
663
+ pass
664
+ finally:
665
+ # A child may ignore TERM even after Bash exits.
666
+ try:
667
+ os.killpg(renderer.pid, signal.SIGKILL)
668
+ except ProcessLookupError:
669
+ pass
670
+ renderer.communicate()
671
+ raise
672
+ except subprocess.TimeoutExpired:
673
+ self._fail(state, "node", f"artifact renderer provisioning timed out after {_RENDERER_INSTALL_TIMEOUT_SECONDS} seconds; retry preparation")
674
+ return False
675
+ except OSError as exc:
676
+ self._fail(state, "node", f"artifact renderer provisioning failed: {exc}")
677
+ return False
678
+ if renderer.returncode != 0:
679
+ detail = (stderr or stdout).strip()
680
+ self._fail(state, "node", f"artifact renderer provisioning failed: {detail}")
681
+ return False
631
682
  self._mark(state, "node", "done", f"{probe.stdout.strip()} at instance-selected path")
632
683
  return True
633
684