create-egregore 0.19.22 → 0.19.23

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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // egregore-launcher-version: 0.19.22
2
+ // egregore-launcher-version: 0.19.23
3
3
  //
4
4
  // egregore — launch and browse Egregore instances from your terminal.
5
5
  // Installed by create-egregore. Reads ~/.egregore/instances.json.
@@ -33,7 +33,7 @@ const crypto = require("node:crypto");
33
33
  const { spawnSync, spawn } = require("node:child_process");
34
34
  const readline = require("node:readline");
35
35
 
36
- const VERSION = "0.19.22";
36
+ const VERSION = "0.19.23";
37
37
  const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
38
38
  const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
39
39
  const UPDATE_CACHE_FILE = path.join(os.homedir(), ".egregore", "update-check.json");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-egregore",
3
- "version": "0.19.22",
3
+ "version": "0.19.23",
4
4
  "description": "Set up Egregore for your team in one command — installs the egregore launcher and your choice of Claude Code, Codex, or Pi.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -22,25 +22,85 @@ an addressed recipient belongs to `$wrap`.
22
22
  `name`, then git config.
23
23
  3. Parse a short topic and optional recipient from the user request. Match the
24
24
  recipient against `memory/people/*.md`; omit the recipient when uncertain.
25
- 4. Draft the markdown body with these sections:
25
+ 4. Draft the canonical markdown record with frontmatter and these sections:
26
26
  - `## Briefing`
27
27
  - `## Key Decisions`
28
28
  - `## Current State`
29
29
  - `## Open Threads`
30
30
  - `## Next Steps`
31
31
  - `## Entry Points`
32
- 5. Write the body to a temp file, then run one orchestration command:
32
+ Use this metadata contract even when the user supplied only prose:
33
+
34
+ ```markdown
35
+ ---
36
+ capture_schema: egregore-capture/v1
37
+ capture_mode: addressed
38
+ kind: addressed
39
+ from: <author>
40
+ addressed_to: <recipient, if any>
41
+ date: YYYY-MM-DD
42
+ topic: <topic>
43
+ intent: <action | feedback | fyi>
44
+ claim: <one-line claim>
45
+ ask: <what the receiver should do>
46
+ ---
47
+ ```
48
+
49
+ 5. Compose the HTML view from that record. Markdown is canonical; the JSON is
50
+ only a presentation map. Give every JSON section a stable `id` and add a
51
+ top-level `sourceMap` from every authored `##` heading to the section that
52
+ contains its complete text:
53
+
54
+ ```json
55
+ {
56
+ "kind": "handoff",
57
+ "kicker": "Handoff",
58
+ "topic": "<topic>",
59
+ "claim": "<one-line claim>",
60
+ "sourceMap": {
61
+ "Briefing": "briefing",
62
+ "Key Decisions": "decisions",
63
+ "Current State": "current-state",
64
+ "Open Threads": "open-threads",
65
+ "Next Steps": "next-steps",
66
+ "Entry Points": "entry-points"
67
+ },
68
+ "sections": [
69
+ {"id":"briefing","label":"Briefing","component":"prose","body":"<complete Briefing text>"}
70
+ ]
71
+ }
72
+ ```
73
+
74
+ Pick richer components when they fit, but copy all authored content into
75
+ the mapped destination. Never independently summarize the Markdown into the
76
+ JSON. For command-heavy sections, keep command headings, explanatory text,
77
+ fenced commands, and expected output verbatim.
78
+
79
+ 6. Render the actual HTML with semantic coverage enabled and show it before
80
+ publishing. This is the approval surface—not a list of component names:
81
+
82
+ ```bash
83
+ node packages/egregore-artifacts/bin/cli.js composed "$COMPOSED_FILE" \
84
+ --source "$BODY_FILE" --verify-fidelity
85
+ ```
86
+
87
+ Stop for confirmation after the browser preview. If validation fails, fix
88
+ the mapping/content and render again. Skip confirmation only when the user
89
+ explicitly said to send without preview.
90
+
91
+ 7. After confirmation, run one orchestration command:
33
92
 
34
93
  ```bash
35
94
  TMPDIR="$(mktemp -d -t codex-handoff-XXXXXX)"
36
- TMPDIR="$TMPDIR" bash bin/capture-run.sh --mode addressed --author "$AUTHOR" --topic "$TOPIC" ${RECIPIENT_ARG} < "$BODY_FILE"
95
+ TMPDIR="$TMPDIR" bash bin/capture-run.sh --mode addressed --author "$AUTHOR" \
96
+ --topic "$TOPIC" ${RECIPIENT_ARG} --composed "$COMPOSED_FILE" < "$BODY_FILE"
37
97
  ```
38
98
 
39
99
  Use `--recipient "$RECIPIENT"` only when a clear teammate or future-self
40
100
  recipient is known. Do not pass `--no-publish` unless the user explicitly
41
101
  asked for a dry run. The runner may plan a notification, but never dispatches.
42
102
 
43
- 6. Read `$TMPDIR/handoff-run-result.json` and render:
103
+ 8. Read `$TMPDIR/handoff-run-result.json` and render:
44
104
 
45
105
  ```bash
46
106
  node bin/codex-skill-render.mjs handoff-card "$TMPDIR/handoff-run-result.json"
@@ -50,20 +110,20 @@ Paste the rendered card exactly in the visible response, preferably in a
50
110
  `text` fenced block. Do not paraphrase the card into bullets unless the user
51
111
  asks for a summary.
52
112
 
53
- 7. If the result has `notifyStatus: "approval_required"`, follow
113
+ 9. If the result has `notifyStatus: "approval_required"`, follow
54
114
  `.claude/context/notification-consent.md`. In a separate checkpoint, show
55
115
  the exact organization, recipient/group, every channel/delivery, and exact
56
116
  message from `notifyPlan`; offer Send / Edit / Cancel and wait. Only a Send
57
117
  response to that preview permits one `approve` + `dispatch`. A local direct
58
118
  message never falls back to a group.
59
119
 
60
- 8. Fire the detached save helper when appropriate:
120
+ 10. Fire the detached save helper when appropriate:
61
121
 
62
122
  ```bash
63
123
  ( bash bin/handoff-save-egregore.sh "$AUTHOR" "$TOPIC" >/dev/null 2>&1 & ) >/dev/null 2>&1
64
124
  ```
65
125
 
66
- 9. Report the `memory/...` path and hosted link from the result JSON when a
126
+ 11. Report the `memory/...` path and hosted link from the result JSON when a
67
127
  link exists.
68
128
 
69
129
  ## Rules
@@ -72,6 +132,8 @@ asks for a summary.
72
132
  - Do not use Claude Code commands.
73
133
  - Keep the briefing concise and operational; include exact files, branches,
74
134
  commands, open questions, and verification state when relevant.
135
+ - The artifact is a view of the handoff, never a summary of it. Publishing
136
+ must fail when any non-empty authored section is unmapped or loses content.
75
137
  - Structured UX parity is required: the rendered card is the acknowledgment.
76
138
  Do not add a preamble such as "Handoff created successfully." Preserve the
77
139
  72-column TUI/card shape, status footer, links below the box, and warning
@@ -309,6 +309,7 @@ function renderHandoff(data) {
309
309
  data.graphStatus ? `graph=${data.graphStatus}` : '',
310
310
  data.memoryStatus ? `memory=${data.memoryStatus}` : '',
311
311
  data.notifyStatus ? `notify=${data.notifyStatus}` : '',
312
+ data.publishStatus ? `publish=${data.publishStatus}` : '',
312
313
  ].filter(Boolean).join(' · ');
313
314
 
314
315
  const lines = [topRule(), padLine('⇌ HANDOFF', data.author || ''), rule()];
@@ -317,6 +318,10 @@ function renderHandoff(data) {
317
318
  if (file) lines.push(padLine(`Path: ${file}`));
318
319
  if (data.artifactUrl) lines.push(padLine(`Link: ${data.artifactUrl}`));
319
320
  if (statuses) lines.push(padLine(statuses));
321
+ if (data.publishStatus === 'fidelity-failed') {
322
+ lines.push(rule());
323
+ lines.push(padLine('Artifact not published — restore missing content and preview again.'));
324
+ }
320
325
  lines.push(bottomRule());
321
326
  return lines.join('\n');
322
327
  }
@@ -122,87 +122,62 @@ if [ ! -s "$ABS_FILE" ]; then
122
122
  exit 1
123
123
  fi
124
124
 
125
- # --- Canonical capture contract -----------------------------------------
126
- # /handoff is the addressed door into the shared capture engine. Persist the
127
- # same schema/mode fields used by /wrap and SessionEnd so filesystem and graph
128
- # retrieval can distinguish captures without inferring from directory names.
129
- if ! grep -qiE '^(capture_schema:|\*\*[Cc]apture [Ss]chema\*\*:)' "$ABS_FILE"; then
130
- CAPTURE_KIND=""
131
- if ! grep -qiE '^(kind:|\*\*[Kk]ind\*\*:)' "$ABS_FILE"; then
132
- CAPTURE_KIND="addressed"
133
- fi
134
- if [ "$(head -1 "$ABS_FILE")" = "---" ]; then
135
- awk -v kind="$CAPTURE_KIND" '
136
- NR == 1 {
137
- print
138
- print "capture_schema: egregore-capture/v1"
139
- print "capture_mode: addressed"
140
- if (kind != "") print "kind: " kind
141
- next
142
- }
143
- { print }
144
- ' "$ABS_FILE" > "$ABS_FILE.new" && mv "$ABS_FILE.new" "$ABS_FILE"
145
- else
146
- awk -v kind="$CAPTURE_KIND" '
147
- !injected && /^\*\*[Aa]uthor\*\*:/ {
148
- print
149
- print "**Capture Schema**: egregore-capture/v1"
150
- print "**Capture Mode**: addressed"
151
- if (kind != "") print "**Kind**: " kind
152
- injected=1
153
- next
154
- }
155
- { print }
156
- END {
157
- if (!injected) {
158
- print "**Capture Schema**: egregore-capture/v1"
159
- print "**Capture Mode**: addressed"
160
- if (kind != "") print "**Kind**: " kind
161
- }
162
- }
163
- ' "$ABS_FILE" > "$ABS_FILE.new" && mv "$ABS_FILE.new" "$ABS_FILE"
164
- fi
165
- fi
125
+ # --- Canonical metadata boundary ----------------------------------------
126
+ # Callers are allowed to send prose-only markdown, but persisted handoffs are
127
+ # not. Normalize every input to the same frontmatter contract before indexing
128
+ # or rendering so title/author/recipient cannot disappear at the adapter seam.
129
+ HAS_FRONTMATTER=0
130
+ [ "$(head -1 "$ABS_FILE")" = "---" ] && HAS_FRONTMATTER=1
131
+
132
+ _has_frontmatter_key() {
133
+ [ "$HAS_FRONTMATTER" = "1" ] && sed -n '2,/^---$/p' "$ABS_FILE" | grep -qiE "^${1}:"
134
+ }
166
135
 
167
- # --- Inject **To**: line if --recipient given and body omits it -----
168
- # index-handoff.sh parses recipients from the file body. Skills
169
- # normally write the To line themselves, but callers that invoke
170
- # handoff-run.sh directly often skip it. Inject after the Author
171
- # line so the graph gets the HANDED_TO edge.
172
- if [ -n "$RECIPIENT" ] && ! grep -qi "^\*\*[Tt]o\*\*:" "$ABS_FILE"; then
173
- awk -v r="$RECIPIENT" '
174
- !injected && /^\*\*[Aa]uthor\*\*:/ {
175
- print
176
- print "**To**: " r
177
- injected=1
178
- next
179
- }
180
- { print }
181
- ' "$ABS_FILE" > "$ABS_FILE.new" && mv "$ABS_FILE.new" "$ABS_FILE"
136
+ MISSING_SCHEMA=0; _has_frontmatter_key capture_schema || MISSING_SCHEMA=1
137
+ MISSING_MODE=0; _has_frontmatter_key capture_mode || MISSING_MODE=1
138
+ MISSING_KIND=0; _has_frontmatter_key kind || MISSING_KIND=1
139
+ MISSING_FROM=0; _has_frontmatter_key from || _has_frontmatter_key author || MISSING_FROM=1
140
+ MISSING_DATE=0; _has_frontmatter_key date || MISSING_DATE=1
141
+ MISSING_TOPIC=0; _has_frontmatter_key topic || MISSING_TOPIC=1
142
+ MISSING_INTENT=0; _has_frontmatter_key intent || MISSING_INTENT=1
143
+ MISSING_RECIPIENT=0
144
+ if [ -n "$RECIPIENT" ]; then
145
+ _has_frontmatter_key addressed_to || _has_frontmatter_key to || MISSING_RECIPIENT=1
182
146
  fi
183
147
 
184
- # --- Persist lifecycle intent in the canonical handoff file -------------
185
- if ! grep -qiE '^(intent:|\*\*[Ii]ntent\*\*:)' "$ABS_FILE"; then
186
- if [ "$(head -1 "$ABS_FILE")" = "---" ]; then
187
- awk -v intent="$INTENT" '
188
- NR == 1 { print; print "intent: " intent; next }
189
- { print }
190
- ' "$ABS_FILE" > "$ABS_FILE.new" && mv "$ABS_FILE.new" "$ABS_FILE"
191
- else
192
- awk -v intent="$INTENT" '
193
- !injected && /^\*\*[Aa]uthor\*\*:/ {
194
- print
195
- print "**Intent**: " intent
196
- injected=1
197
- next
198
- }
199
- { print }
200
- END {
201
- if (!injected) print "**Intent**: " intent
202
- }
203
- ' "$ABS_FILE" > "$ABS_FILE.new" && mv "$ABS_FILE.new" "$ABS_FILE"
204
- fi
205
- fi
148
+ awk \
149
+ -v has_frontmatter="$HAS_FRONTMATTER" \
150
+ -v schema="$MISSING_SCHEMA" -v mode="$MISSING_MODE" -v kind="$MISSING_KIND" \
151
+ -v from="$MISSING_FROM" -v date_missing="$MISSING_DATE" \
152
+ -v topic_missing="$MISSING_TOPIC" -v intent_missing="$MISSING_INTENT" \
153
+ -v recipient_missing="$MISSING_RECIPIENT" \
154
+ -v author="$AUTHOR" -v date="$TODAY" -v topic="$TOPIC" \
155
+ -v intent="$INTENT" -v recipient="$RECIPIENT" '
156
+ function metadata() {
157
+ if (schema) print "capture_schema: egregore-capture/v1"
158
+ if (mode) print "capture_mode: addressed"
159
+ if (kind) print "kind: addressed"
160
+ if (from) print "from: " author
161
+ if (recipient_missing) print "addressed_to: " recipient
162
+ if (date_missing) print "date: " date
163
+ if (topic_missing) print "topic: " topic
164
+ if (intent_missing) print "intent: " intent
165
+ }
166
+ BEGIN {
167
+ if (!has_frontmatter) {
168
+ print "---"
169
+ metadata()
170
+ print "---"
171
+ print ""
172
+ }
173
+ }
174
+ has_frontmatter && NR == 1 {
175
+ print
176
+ metadata()
177
+ next
178
+ }
179
+ { print }
180
+ ' "$ABS_FILE" > "$ABS_FILE.new" && mv "$ABS_FILE.new" "$ABS_FILE"
206
181
 
207
182
  # --- Append repo state ---------------------------------------------------
208
183
  # --no-pr skips `gh pr list` (~400–600ms per repo). A detached PR backfill
@@ -401,12 +376,16 @@ PID_MEMORY=$!
401
376
  RENDERED_HTML="$TMPD/handoff.html"
402
377
  RENDER_OK=0
403
378
  LOCAL_CLI="$SCRIPT_DIR/packages/egregore-artifacts/bin/cli.js"
379
+ FIDELITY_ARGS=(--verify-fidelity)
380
+ if [ "$RENDER_TYPE" = "composed" ]; then
381
+ FIDELITY_ARGS+=(--source "$ABS_FILE")
382
+ fi
404
383
  if [ "${EGREGORE_USE_PUBLISHED:-0}" != "1" ] && [ -f "$LOCAL_CLI" ] && [ -d "$SCRIPT_DIR/packages/egregore-artifacts/node_modules/react" ]; then
405
- if node "$LOCAL_CLI" "$RENDER_TYPE" "${RENDER_SRC:-$ABS_FILE}" --output "$RENDERED_HTML" >/dev/null 2>&1; then
384
+ if node "$LOCAL_CLI" "$RENDER_TYPE" "${RENDER_SRC:-$ABS_FILE}" "${FIDELITY_ARGS[@]}" --output "$RENDERED_HTML" >/dev/null 2>"$TMPD/render-error"; then
406
385
  RENDER_OK=1
407
386
  fi
408
387
  else
409
- if npx egregore-artifacts "$RENDER_TYPE" "${RENDER_SRC:-$ABS_FILE}" --output "$RENDERED_HTML" >/dev/null 2>&1; then
388
+ if npx -y egregore-artifacts@latest "$RENDER_TYPE" "${RENDER_SRC:-$ABS_FILE}" "${FIDELITY_ARGS[@]}" --output "$RENDERED_HTML" >/dev/null 2>"$TMPD/render-error"; then
410
389
  RENDER_OK=1
411
390
  fi
412
391
  fi
@@ -481,9 +460,11 @@ PID_MEMORY=$!
481
460
  PUBLISH_RC=0
482
461
  if [ -z "$V1URL" ] && [ "$NO_PUBLISH" = "0" ] && [ -x "$SCRIPT_DIR/bin/publish-artifact.sh" ]; then
483
462
  AURL=$(bash "$SCRIPT_DIR/bin/publish-artifact.sh" "$RENDER_TYPE" "${RENDER_SRC:-$ABS_FILE}" \
463
+ --verify-fidelity \
464
+ --source "$ABS_FILE" \
484
465
  --title "$TOPIC" \
485
466
  --author "$AUTHOR" \
486
- --description "$BRIEFING_LEAD" 2>/dev/null) || PUBLISH_RC=$?
467
+ --description "$BRIEFING_LEAD" 2>"$TMPD/publish-error") || PUBLISH_RC=$?
487
468
  fi
488
469
 
489
470
  PS="published"
@@ -495,6 +476,8 @@ PID_MEMORY=$!
495
476
  PS="relay-off"
496
477
  elif [ "$PUBLISH_RC" = "3" ]; then
497
478
  PS="hosting-off"
479
+ elif grep -q 'Handoff fidelity check failed' "$TMPD/render-error" "$TMPD/publish-error" 2>/dev/null; then
480
+ PS="fidelity-failed"
498
481
  else
499
482
  PS="failed"
500
483
  fi
@@ -614,6 +597,7 @@ STATUS_BITS=("saved")
614
597
  [ "$NOTIFY_STATUS" = "approval_required" ] && STATUS_BITS+=("notify approval pending")
615
598
  [ -n "$ARTIFACT_URL" ] && STATUS_BITS+=("published")
616
599
  [ "$PUBLISH_STATUS" = "relay-off" ] && STATUS_BITS+=("not published")
600
+ [ "$PUBLISH_STATUS" = "fidelity-failed" ] && STATUS_BITS+=("artifact fidelity failed")
617
601
 
618
602
  STATUS_LINE=""
619
603
  for bit in "${STATUS_BITS[@]}"; do
@@ -119,11 +119,12 @@ AUTHOR="$(md_field 'Author')"
119
119
  AUTHOR_HANDLE="$(echo "$AUTHOR" | sed 's/[[:space:]]*→.*//' | awk '{print tolower($1)}')"
120
120
  [ -z "$AUTHOR_HANDLE" ] && AUTHOR_HANDLE="unknown"
121
121
 
122
- # Recipients: try markdown header (To or For), then YAML (to or for)
122
+ # Recipients: try markdown header, then canonical/legacy YAML names.
123
123
  RECIPIENTS_RAW="$(md_field 'To')"
124
124
  [ -z "$RECIPIENTS_RAW" ] && RECIPIENTS_RAW="$(md_field 'For')"
125
125
  [ -z "$RECIPIENTS_RAW" ] && RECIPIENTS_RAW="$(yaml_extract 'to')"
126
126
  [ -z "$RECIPIENTS_RAW" ] && RECIPIENTS_RAW="$(yaml_extract 'for')"
127
+ [ -z "$RECIPIENTS_RAW" ] && RECIPIENTS_RAW="$(yaml_extract 'addressed_to')"
127
128
  # Also extract from "From: author → recipient" pattern
128
129
  if [ -z "$RECIPIENTS_RAW" ] && echo "$AUTHOR" | grep -q '→' 2>/dev/null; then
129
130
  RECIPIENTS_RAW="$(echo "$AUTHOR" | sed 's/.*→[[:space:]]*//')"
@@ -27,6 +27,7 @@ set -euo pipefail
27
27
  # 3 hosting turned off (features.publishing=false)
28
28
  # 4 nothing uploaded — public relay not enabled and no org API key,
29
29
  # or egregore.json is missing/unreadable (fail closed)
30
+ # 5 renderer/fidelity validation failed (nothing uploaded)
30
31
 
31
32
  SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
32
33
  CONFIG="$SCRIPT_DIR/egregore.json"
@@ -41,6 +42,8 @@ ARTIFACT_ID=""
41
42
  RAW_HTML=0
42
43
  SKIP_REFERENCES=0
43
44
  PAIR=""
45
+ VERIFY_FIDELITY=0
46
+ FIDELITY_SOURCE=""
44
47
 
45
48
  while [[ $# -gt 0 ]]; do
46
49
  case "$1" in
@@ -49,6 +52,8 @@ while [[ $# -gt 0 ]]; do
49
52
  --description) DESCRIPTION="$2"; shift 2 ;;
50
53
  --id) ARTIFACT_ID="$2"; shift 2 ;;
51
54
  --pair) PAIR="$2"; shift 2 ;;
55
+ --verify-fidelity) VERIFY_FIDELITY=1; shift ;;
56
+ --source) FIDELITY_SOURCE="$2"; shift 2 ;;
52
57
  --raw-html) RAW_HTML=1; shift ;;
53
58
  --no-references) SKIP_REFERENCES=1; shift ;;
54
59
  *)
@@ -165,6 +170,16 @@ else
165
170
  if [ -n "$PAIR" ]; then
166
171
  RENDER_ARGS+=(--pair "$PAIR")
167
172
  fi
173
+ if [ "$VERIFY_FIDELITY" -eq 1 ]; then
174
+ RENDER_ARGS+=(--verify-fidelity)
175
+ if [ "$TYPE" = "composed" ]; then
176
+ [ -n "$FIDELITY_SOURCE" ] || {
177
+ echo "Handoff fidelity check requires --source <canonical-handoff.md>" >&2
178
+ exit 1
179
+ }
180
+ RENDER_ARGS+=(--source "$FIDELITY_SOURCE")
181
+ fi
182
+ fi
168
183
 
169
184
  # Prefer the in-repo package copy when it's present AND its dependencies
170
185
  # have been installed — that way local edits to packages/egregore-artifacts
@@ -174,15 +189,21 @@ else
174
189
  # when a local install is present — useful in the upstream dev repo to
175
190
  # dogfood what teammates and OSS users actually run.
176
191
  LOCAL_CLI="$SCRIPT_DIR/packages/egregore-artifacts/bin/cli.js"
192
+ RENDER_ERROR="/tmp/egregore-artifacts/render-error-$$.log"
177
193
  if [ "${EGREGORE_USE_PUBLISHED:-0}" != "1" ] && [ -f "$LOCAL_CLI" ] && [ -d "$SCRIPT_DIR/packages/egregore-artifacts/node_modules/react" ]; then
178
- if ! node "$LOCAL_CLI" "${RENDER_ARGS[@]}" >/dev/null 2>&1; then
179
- exit 0
194
+ if ! node "$LOCAL_CLI" "${RENDER_ARGS[@]}" >/dev/null 2>"$RENDER_ERROR"; then
195
+ sed -n '1,4p' "$RENDER_ERROR" >&2
196
+ rm -f "$RENDER_ERROR"
197
+ exit 5
180
198
  fi
181
199
  else
182
- if ! npx egregore-artifacts@latest "${RENDER_ARGS[@]}" >/dev/null 2>&1; then
183
- exit 0
200
+ if ! npx -y egregore-artifacts@latest "${RENDER_ARGS[@]}" >/dev/null 2>"$RENDER_ERROR"; then
201
+ sed -n '1,4p' "$RENDER_ERROR" >&2
202
+ rm -f "$RENDER_ERROR"
203
+ exit 5
184
204
  fi
185
205
  fi
206
+ rm -f "$RENDER_ERROR"
186
207
  fi
187
208
 
188
209
  if [ ! -f "$TMP_HTML" ]; then
@@ -22,25 +22,85 @@ an addressed recipient belongs to `$wrap`.
22
22
  `name`, then git config.
23
23
  3. Parse a short topic and optional recipient from the user request. Match the
24
24
  recipient against `memory/people/*.md`; omit the recipient when uncertain.
25
- 4. Draft the markdown body with these sections:
25
+ 4. Draft the canonical markdown record with frontmatter and these sections:
26
26
  - `## Briefing`
27
27
  - `## Key Decisions`
28
28
  - `## Current State`
29
29
  - `## Open Threads`
30
30
  - `## Next Steps`
31
31
  - `## Entry Points`
32
- 5. Write the body to a temp file, then run one orchestration command:
32
+ Use this metadata contract even when the user supplied only prose:
33
+
34
+ ```markdown
35
+ ---
36
+ capture_schema: egregore-capture/v1
37
+ capture_mode: addressed
38
+ kind: addressed
39
+ from: <author>
40
+ addressed_to: <recipient, if any>
41
+ date: YYYY-MM-DD
42
+ topic: <topic>
43
+ intent: <action | feedback | fyi>
44
+ claim: <one-line claim>
45
+ ask: <what the receiver should do>
46
+ ---
47
+ ```
48
+
49
+ 5. Compose the HTML view from that record. Markdown is canonical; the JSON is
50
+ only a presentation map. Give every JSON section a stable `id` and add a
51
+ top-level `sourceMap` from every authored `##` heading to the section that
52
+ contains its complete text:
53
+
54
+ ```json
55
+ {
56
+ "kind": "handoff",
57
+ "kicker": "Handoff",
58
+ "topic": "<topic>",
59
+ "claim": "<one-line claim>",
60
+ "sourceMap": {
61
+ "Briefing": "briefing",
62
+ "Key Decisions": "decisions",
63
+ "Current State": "current-state",
64
+ "Open Threads": "open-threads",
65
+ "Next Steps": "next-steps",
66
+ "Entry Points": "entry-points"
67
+ },
68
+ "sections": [
69
+ {"id":"briefing","label":"Briefing","component":"prose","body":"<complete Briefing text>"}
70
+ ]
71
+ }
72
+ ```
73
+
74
+ Pick richer components when they fit, but copy all authored content into
75
+ the mapped destination. Never independently summarize the Markdown into the
76
+ JSON. For command-heavy sections, keep command headings, explanatory text,
77
+ fenced commands, and expected output verbatim.
78
+
79
+ 6. Render the actual HTML with semantic coverage enabled and show it before
80
+ publishing. This is the approval surface—not a list of component names:
81
+
82
+ ```bash
83
+ node packages/egregore-artifacts/bin/cli.js composed "$COMPOSED_FILE" \
84
+ --source "$BODY_FILE" --verify-fidelity
85
+ ```
86
+
87
+ Stop for confirmation after the browser preview. If validation fails, fix
88
+ the mapping/content and render again. Skip confirmation only when the user
89
+ explicitly said to send without preview.
90
+
91
+ 7. After confirmation, run one orchestration command:
33
92
 
34
93
  ```bash
35
94
  TMPDIR="$(mktemp -d -t codex-handoff-XXXXXX)"
36
- TMPDIR="$TMPDIR" bash bin/capture-run.sh --mode addressed --author "$AUTHOR" --topic "$TOPIC" ${RECIPIENT_ARG} < "$BODY_FILE"
95
+ TMPDIR="$TMPDIR" bash bin/capture-run.sh --mode addressed --author "$AUTHOR" \
96
+ --topic "$TOPIC" ${RECIPIENT_ARG} --composed "$COMPOSED_FILE" < "$BODY_FILE"
37
97
  ```
38
98
 
39
99
  Use `--recipient "$RECIPIENT"` only when a clear teammate or future-self
40
100
  recipient is known. Do not pass `--no-publish` unless the user explicitly
41
101
  asked for a dry run. The runner may plan a notification, but never dispatches.
42
102
 
43
- 6. Read `$TMPDIR/handoff-run-result.json` and render:
103
+ 8. Read `$TMPDIR/handoff-run-result.json` and render:
44
104
 
45
105
  ```bash
46
106
  node bin/codex-skill-render.mjs handoff-card "$TMPDIR/handoff-run-result.json"
@@ -50,20 +110,20 @@ Paste the rendered card exactly in the visible response, preferably in a
50
110
  `text` fenced block. Do not paraphrase the card into bullets unless the user
51
111
  asks for a summary.
52
112
 
53
- 7. If the result has `notifyStatus: "approval_required"`, follow
113
+ 9. If the result has `notifyStatus: "approval_required"`, follow
54
114
  `.claude/context/notification-consent.md`. In a separate checkpoint, show
55
115
  the exact organization, recipient/group, every channel/delivery, and exact
56
116
  message from `notifyPlan`; offer Send / Edit / Cancel and wait. Only a Send
57
117
  response to that preview permits one `approve` + `dispatch`. A local direct
58
118
  message never falls back to a group.
59
119
 
60
- 8. Fire the detached save helper when appropriate:
120
+ 10. Fire the detached save helper when appropriate:
61
121
 
62
122
  ```bash
63
123
  ( bash bin/handoff-save-egregore.sh "$AUTHOR" "$TOPIC" >/dev/null 2>&1 & ) >/dev/null 2>&1
64
124
  ```
65
125
 
66
- 9. Report the `memory/...` path and hosted link from the result JSON when a
126
+ 11. Report the `memory/...` path and hosted link from the result JSON when a
67
127
  link exists.
68
128
 
69
129
  ## Rules
@@ -72,6 +132,8 @@ asks for a summary.
72
132
  - Do not use Claude Code commands.
73
133
  - Keep the briefing concise and operational; include exact files, branches,
74
134
  commands, open questions, and verification state when relevant.
135
+ - The artifact is a view of the handoff, never a summary of it. Publishing
136
+ must fail when any non-empty authored section is unmapped or loses content.
75
137
  - Structured UX parity is required: the rendered card is the acknowledgment.
76
138
  Do not add a preamble such as "Handoff created successfully." Preserve the
77
139
  72-column TUI/card shape, status footer, links below the box, and warning
@@ -309,6 +309,7 @@ function renderHandoff(data) {
309
309
  data.graphStatus ? `graph=${data.graphStatus}` : '',
310
310
  data.memoryStatus ? `memory=${data.memoryStatus}` : '',
311
311
  data.notifyStatus ? `notify=${data.notifyStatus}` : '',
312
+ data.publishStatus ? `publish=${data.publishStatus}` : '',
312
313
  ].filter(Boolean).join(' · ');
313
314
 
314
315
  const lines = [topRule(), padLine('⇌ HANDOFF', data.author || ''), rule()];
@@ -317,6 +318,10 @@ function renderHandoff(data) {
317
318
  if (file) lines.push(padLine(`Path: ${file}`));
318
319
  if (data.artifactUrl) lines.push(padLine(`Link: ${data.artifactUrl}`));
319
320
  if (statuses) lines.push(padLine(statuses));
321
+ if (data.publishStatus === 'fidelity-failed') {
322
+ lines.push(rule());
323
+ lines.push(padLine('Artifact not published — restore missing content and preview again.'));
324
+ }
320
325
  lines.push(bottomRule());
321
326
  return lines.join('\n');
322
327
  }
@@ -122,87 +122,62 @@ if [ ! -s "$ABS_FILE" ]; then
122
122
  exit 1
123
123
  fi
124
124
 
125
- # --- Canonical capture contract -----------------------------------------
126
- # /handoff is the addressed door into the shared capture engine. Persist the
127
- # same schema/mode fields used by /wrap and SessionEnd so filesystem and graph
128
- # retrieval can distinguish captures without inferring from directory names.
129
- if ! grep -qiE '^(capture_schema:|\*\*[Cc]apture [Ss]chema\*\*:)' "$ABS_FILE"; then
130
- CAPTURE_KIND=""
131
- if ! grep -qiE '^(kind:|\*\*[Kk]ind\*\*:)' "$ABS_FILE"; then
132
- CAPTURE_KIND="addressed"
133
- fi
134
- if [ "$(head -1 "$ABS_FILE")" = "---" ]; then
135
- awk -v kind="$CAPTURE_KIND" '
136
- NR == 1 {
137
- print
138
- print "capture_schema: egregore-capture/v1"
139
- print "capture_mode: addressed"
140
- if (kind != "") print "kind: " kind
141
- next
142
- }
143
- { print }
144
- ' "$ABS_FILE" > "$ABS_FILE.new" && mv "$ABS_FILE.new" "$ABS_FILE"
145
- else
146
- awk -v kind="$CAPTURE_KIND" '
147
- !injected && /^\*\*[Aa]uthor\*\*:/ {
148
- print
149
- print "**Capture Schema**: egregore-capture/v1"
150
- print "**Capture Mode**: addressed"
151
- if (kind != "") print "**Kind**: " kind
152
- injected=1
153
- next
154
- }
155
- { print }
156
- END {
157
- if (!injected) {
158
- print "**Capture Schema**: egregore-capture/v1"
159
- print "**Capture Mode**: addressed"
160
- if (kind != "") print "**Kind**: " kind
161
- }
162
- }
163
- ' "$ABS_FILE" > "$ABS_FILE.new" && mv "$ABS_FILE.new" "$ABS_FILE"
164
- fi
165
- fi
125
+ # --- Canonical metadata boundary ----------------------------------------
126
+ # Callers are allowed to send prose-only markdown, but persisted handoffs are
127
+ # not. Normalize every input to the same frontmatter contract before indexing
128
+ # or rendering so title/author/recipient cannot disappear at the adapter seam.
129
+ HAS_FRONTMATTER=0
130
+ [ "$(head -1 "$ABS_FILE")" = "---" ] && HAS_FRONTMATTER=1
131
+
132
+ _has_frontmatter_key() {
133
+ [ "$HAS_FRONTMATTER" = "1" ] && sed -n '2,/^---$/p' "$ABS_FILE" | grep -qiE "^${1}:"
134
+ }
166
135
 
167
- # --- Inject **To**: line if --recipient given and body omits it -----
168
- # index-handoff.sh parses recipients from the file body. Skills
169
- # normally write the To line themselves, but callers that invoke
170
- # handoff-run.sh directly often skip it. Inject after the Author
171
- # line so the graph gets the HANDED_TO edge.
172
- if [ -n "$RECIPIENT" ] && ! grep -qi "^\*\*[Tt]o\*\*:" "$ABS_FILE"; then
173
- awk -v r="$RECIPIENT" '
174
- !injected && /^\*\*[Aa]uthor\*\*:/ {
175
- print
176
- print "**To**: " r
177
- injected=1
178
- next
179
- }
180
- { print }
181
- ' "$ABS_FILE" > "$ABS_FILE.new" && mv "$ABS_FILE.new" "$ABS_FILE"
136
+ MISSING_SCHEMA=0; _has_frontmatter_key capture_schema || MISSING_SCHEMA=1
137
+ MISSING_MODE=0; _has_frontmatter_key capture_mode || MISSING_MODE=1
138
+ MISSING_KIND=0; _has_frontmatter_key kind || MISSING_KIND=1
139
+ MISSING_FROM=0; _has_frontmatter_key from || _has_frontmatter_key author || MISSING_FROM=1
140
+ MISSING_DATE=0; _has_frontmatter_key date || MISSING_DATE=1
141
+ MISSING_TOPIC=0; _has_frontmatter_key topic || MISSING_TOPIC=1
142
+ MISSING_INTENT=0; _has_frontmatter_key intent || MISSING_INTENT=1
143
+ MISSING_RECIPIENT=0
144
+ if [ -n "$RECIPIENT" ]; then
145
+ _has_frontmatter_key addressed_to || _has_frontmatter_key to || MISSING_RECIPIENT=1
182
146
  fi
183
147
 
184
- # --- Persist lifecycle intent in the canonical handoff file -------------
185
- if ! grep -qiE '^(intent:|\*\*[Ii]ntent\*\*:)' "$ABS_FILE"; then
186
- if [ "$(head -1 "$ABS_FILE")" = "---" ]; then
187
- awk -v intent="$INTENT" '
188
- NR == 1 { print; print "intent: " intent; next }
189
- { print }
190
- ' "$ABS_FILE" > "$ABS_FILE.new" && mv "$ABS_FILE.new" "$ABS_FILE"
191
- else
192
- awk -v intent="$INTENT" '
193
- !injected && /^\*\*[Aa]uthor\*\*:/ {
194
- print
195
- print "**Intent**: " intent
196
- injected=1
197
- next
198
- }
199
- { print }
200
- END {
201
- if (!injected) print "**Intent**: " intent
202
- }
203
- ' "$ABS_FILE" > "$ABS_FILE.new" && mv "$ABS_FILE.new" "$ABS_FILE"
204
- fi
205
- fi
148
+ awk \
149
+ -v has_frontmatter="$HAS_FRONTMATTER" \
150
+ -v schema="$MISSING_SCHEMA" -v mode="$MISSING_MODE" -v kind="$MISSING_KIND" \
151
+ -v from="$MISSING_FROM" -v date_missing="$MISSING_DATE" \
152
+ -v topic_missing="$MISSING_TOPIC" -v intent_missing="$MISSING_INTENT" \
153
+ -v recipient_missing="$MISSING_RECIPIENT" \
154
+ -v author="$AUTHOR" -v date="$TODAY" -v topic="$TOPIC" \
155
+ -v intent="$INTENT" -v recipient="$RECIPIENT" '
156
+ function metadata() {
157
+ if (schema) print "capture_schema: egregore-capture/v1"
158
+ if (mode) print "capture_mode: addressed"
159
+ if (kind) print "kind: addressed"
160
+ if (from) print "from: " author
161
+ if (recipient_missing) print "addressed_to: " recipient
162
+ if (date_missing) print "date: " date
163
+ if (topic_missing) print "topic: " topic
164
+ if (intent_missing) print "intent: " intent
165
+ }
166
+ BEGIN {
167
+ if (!has_frontmatter) {
168
+ print "---"
169
+ metadata()
170
+ print "---"
171
+ print ""
172
+ }
173
+ }
174
+ has_frontmatter && NR == 1 {
175
+ print
176
+ metadata()
177
+ next
178
+ }
179
+ { print }
180
+ ' "$ABS_FILE" > "$ABS_FILE.new" && mv "$ABS_FILE.new" "$ABS_FILE"
206
181
 
207
182
  # --- Append repo state ---------------------------------------------------
208
183
  # --no-pr skips `gh pr list` (~400–600ms per repo). A detached PR backfill
@@ -401,12 +376,16 @@ PID_MEMORY=$!
401
376
  RENDERED_HTML="$TMPD/handoff.html"
402
377
  RENDER_OK=0
403
378
  LOCAL_CLI="$SCRIPT_DIR/packages/egregore-artifacts/bin/cli.js"
379
+ FIDELITY_ARGS=(--verify-fidelity)
380
+ if [ "$RENDER_TYPE" = "composed" ]; then
381
+ FIDELITY_ARGS+=(--source "$ABS_FILE")
382
+ fi
404
383
  if [ "${EGREGORE_USE_PUBLISHED:-0}" != "1" ] && [ -f "$LOCAL_CLI" ] && [ -d "$SCRIPT_DIR/packages/egregore-artifacts/node_modules/react" ]; then
405
- if node "$LOCAL_CLI" "$RENDER_TYPE" "${RENDER_SRC:-$ABS_FILE}" --output "$RENDERED_HTML" >/dev/null 2>&1; then
384
+ if node "$LOCAL_CLI" "$RENDER_TYPE" "${RENDER_SRC:-$ABS_FILE}" "${FIDELITY_ARGS[@]}" --output "$RENDERED_HTML" >/dev/null 2>"$TMPD/render-error"; then
406
385
  RENDER_OK=1
407
386
  fi
408
387
  else
409
- if npx egregore-artifacts "$RENDER_TYPE" "${RENDER_SRC:-$ABS_FILE}" --output "$RENDERED_HTML" >/dev/null 2>&1; then
388
+ if npx -y egregore-artifacts@latest "$RENDER_TYPE" "${RENDER_SRC:-$ABS_FILE}" "${FIDELITY_ARGS[@]}" --output "$RENDERED_HTML" >/dev/null 2>"$TMPD/render-error"; then
410
389
  RENDER_OK=1
411
390
  fi
412
391
  fi
@@ -481,9 +460,11 @@ PID_MEMORY=$!
481
460
  PUBLISH_RC=0
482
461
  if [ -z "$V1URL" ] && [ "$NO_PUBLISH" = "0" ] && [ -x "$SCRIPT_DIR/bin/publish-artifact.sh" ]; then
483
462
  AURL=$(bash "$SCRIPT_DIR/bin/publish-artifact.sh" "$RENDER_TYPE" "${RENDER_SRC:-$ABS_FILE}" \
463
+ --verify-fidelity \
464
+ --source "$ABS_FILE" \
484
465
  --title "$TOPIC" \
485
466
  --author "$AUTHOR" \
486
- --description "$BRIEFING_LEAD" 2>/dev/null) || PUBLISH_RC=$?
467
+ --description "$BRIEFING_LEAD" 2>"$TMPD/publish-error") || PUBLISH_RC=$?
487
468
  fi
488
469
 
489
470
  PS="published"
@@ -495,6 +476,8 @@ PID_MEMORY=$!
495
476
  PS="relay-off"
496
477
  elif [ "$PUBLISH_RC" = "3" ]; then
497
478
  PS="hosting-off"
479
+ elif grep -q 'Handoff fidelity check failed' "$TMPD/render-error" "$TMPD/publish-error" 2>/dev/null; then
480
+ PS="fidelity-failed"
498
481
  else
499
482
  PS="failed"
500
483
  fi
@@ -614,6 +597,7 @@ STATUS_BITS=("saved")
614
597
  [ "$NOTIFY_STATUS" = "approval_required" ] && STATUS_BITS+=("notify approval pending")
615
598
  [ -n "$ARTIFACT_URL" ] && STATUS_BITS+=("published")
616
599
  [ "$PUBLISH_STATUS" = "relay-off" ] && STATUS_BITS+=("not published")
600
+ [ "$PUBLISH_STATUS" = "fidelity-failed" ] && STATUS_BITS+=("artifact fidelity failed")
617
601
 
618
602
  STATUS_LINE=""
619
603
  for bit in "${STATUS_BITS[@]}"; do
@@ -119,11 +119,12 @@ AUTHOR="$(md_field 'Author')"
119
119
  AUTHOR_HANDLE="$(echo "$AUTHOR" | sed 's/[[:space:]]*→.*//' | awk '{print tolower($1)}')"
120
120
  [ -z "$AUTHOR_HANDLE" ] && AUTHOR_HANDLE="unknown"
121
121
 
122
- # Recipients: try markdown header (To or For), then YAML (to or for)
122
+ # Recipients: try markdown header, then canonical/legacy YAML names.
123
123
  RECIPIENTS_RAW="$(md_field 'To')"
124
124
  [ -z "$RECIPIENTS_RAW" ] && RECIPIENTS_RAW="$(md_field 'For')"
125
125
  [ -z "$RECIPIENTS_RAW" ] && RECIPIENTS_RAW="$(yaml_extract 'to')"
126
126
  [ -z "$RECIPIENTS_RAW" ] && RECIPIENTS_RAW="$(yaml_extract 'for')"
127
+ [ -z "$RECIPIENTS_RAW" ] && RECIPIENTS_RAW="$(yaml_extract 'addressed_to')"
127
128
  # Also extract from "From: author → recipient" pattern
128
129
  if [ -z "$RECIPIENTS_RAW" ] && echo "$AUTHOR" | grep -q '→' 2>/dev/null; then
129
130
  RECIPIENTS_RAW="$(echo "$AUTHOR" | sed 's/.*→[[:space:]]*//')"
@@ -199,14 +199,23 @@ function section(title, body) {
199
199
  }
200
200
 
201
201
  export function buildHandoffDocument(params, author, date = new Date().toISOString().slice(0, 10)) {
202
+ const topic = cleanArgs(params.topic);
203
+ const recipient = cleanArgs(params.recipient);
202
204
  const lines = [
203
- `# Handoff: ${cleanArgs(params.topic)}`,
204
- "",
205
- `**Date**: ${date}`,
206
- `**Author**: ${author}`,
205
+ "---",
206
+ "capture_schema: egregore-capture/v1",
207
+ "capture_mode: addressed",
208
+ "kind: addressed",
209
+ `from: ${author}`,
207
210
  ];
208
- if (cleanArgs(params.recipient)) lines.push(`**To**: ${cleanArgs(params.recipient)}`);
211
+ if (recipient) lines.push(`addressed_to: ${recipient}`);
209
212
  lines.push(
213
+ `date: ${date}`,
214
+ `topic: ${topic}`,
215
+ "intent: action",
216
+ "---",
217
+ "",
218
+ `# Handoff: ${topic}`,
210
219
  "",
211
220
  section("Briefing", params.briefing),
212
221
  "",
@@ -27,6 +27,7 @@ set -euo pipefail
27
27
  # 3 hosting turned off (features.publishing=false)
28
28
  # 4 nothing uploaded — public relay not enabled and no org API key,
29
29
  # or egregore.json is missing/unreadable (fail closed)
30
+ # 5 renderer/fidelity validation failed (nothing uploaded)
30
31
 
31
32
  SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
32
33
  CONFIG="$SCRIPT_DIR/egregore.json"
@@ -41,6 +42,8 @@ ARTIFACT_ID=""
41
42
  RAW_HTML=0
42
43
  SKIP_REFERENCES=0
43
44
  PAIR=""
45
+ VERIFY_FIDELITY=0
46
+ FIDELITY_SOURCE=""
44
47
 
45
48
  while [[ $# -gt 0 ]]; do
46
49
  case "$1" in
@@ -49,6 +52,8 @@ while [[ $# -gt 0 ]]; do
49
52
  --description) DESCRIPTION="$2"; shift 2 ;;
50
53
  --id) ARTIFACT_ID="$2"; shift 2 ;;
51
54
  --pair) PAIR="$2"; shift 2 ;;
55
+ --verify-fidelity) VERIFY_FIDELITY=1; shift ;;
56
+ --source) FIDELITY_SOURCE="$2"; shift 2 ;;
52
57
  --raw-html) RAW_HTML=1; shift ;;
53
58
  --no-references) SKIP_REFERENCES=1; shift ;;
54
59
  *)
@@ -165,6 +170,16 @@ else
165
170
  if [ -n "$PAIR" ]; then
166
171
  RENDER_ARGS+=(--pair "$PAIR")
167
172
  fi
173
+ if [ "$VERIFY_FIDELITY" -eq 1 ]; then
174
+ RENDER_ARGS+=(--verify-fidelity)
175
+ if [ "$TYPE" = "composed" ]; then
176
+ [ -n "$FIDELITY_SOURCE" ] || {
177
+ echo "Handoff fidelity check requires --source <canonical-handoff.md>" >&2
178
+ exit 1
179
+ }
180
+ RENDER_ARGS+=(--source "$FIDELITY_SOURCE")
181
+ fi
182
+ fi
168
183
 
169
184
  # Prefer the in-repo package copy when it's present AND its dependencies
170
185
  # have been installed — that way local edits to packages/egregore-artifacts
@@ -174,15 +189,21 @@ else
174
189
  # when a local install is present — useful in the upstream dev repo to
175
190
  # dogfood what teammates and OSS users actually run.
176
191
  LOCAL_CLI="$SCRIPT_DIR/packages/egregore-artifacts/bin/cli.js"
192
+ RENDER_ERROR="/tmp/egregore-artifacts/render-error-$$.log"
177
193
  if [ "${EGREGORE_USE_PUBLISHED:-0}" != "1" ] && [ -f "$LOCAL_CLI" ] && [ -d "$SCRIPT_DIR/packages/egregore-artifacts/node_modules/react" ]; then
178
- if ! node "$LOCAL_CLI" "${RENDER_ARGS[@]}" >/dev/null 2>&1; then
179
- exit 0
194
+ if ! node "$LOCAL_CLI" "${RENDER_ARGS[@]}" >/dev/null 2>"$RENDER_ERROR"; then
195
+ sed -n '1,4p' "$RENDER_ERROR" >&2
196
+ rm -f "$RENDER_ERROR"
197
+ exit 5
180
198
  fi
181
199
  else
182
- if ! npx egregore-artifacts@latest "${RENDER_ARGS[@]}" >/dev/null 2>&1; then
183
- exit 0
200
+ if ! npx -y egregore-artifacts@latest "${RENDER_ARGS[@]}" >/dev/null 2>"$RENDER_ERROR"; then
201
+ sed -n '1,4p' "$RENDER_ERROR" >&2
202
+ rm -f "$RENDER_ERROR"
203
+ exit 5
184
204
  fi
185
205
  fi
206
+ rm -f "$RENDER_ERROR"
186
207
  fi
187
208
 
188
209
  if [ ! -f "$TMP_HTML" ]; then