diff --git a/apps/decodex/src/radar.rs b/apps/decodex/src/radar.rs index a4936ae..87444e6 100644 --- a/apps/decodex/src/radar.rs +++ b/apps/decodex/src/radar.rs @@ -38,7 +38,7 @@ const DEFAULT_SIGNALS_DIR: &str = "site/src/content/signals"; const DEFAULT_STABLE_LIMIT: usize = 0; const DEFAULT_TAG_PREFIX: &str = "rust-v"; const RELEASE_DELTA_SCHEMA: &str = "release_delta/v1"; -const SCHEMA_VERSION: i64 = 2; +const SCHEMA_VERSION: i64 = 3; const SIGNAL_SCHEMA: &str = "signal_entry/v1"; const SOCIAL_CANDIDATE_SCHEMA: &str = "social_candidate/v1"; const SOCIAL_POST_SCHEMA: &str = "social_post/v1"; @@ -77,14 +77,8 @@ const SOCIAL_POST_WORTHINESS: &[&str] = &["block", "publish", "skip"]; const SOURCE_ITEM_KINDS: &[&str] = &["commit", "pull_request"]; const UPSTREAM_IMPACT_KINDS: &[&str] = &["browser_observation", "changelog", "commit", "pull_request", "release", "signal"]; -const UPSTREAM_REVIEW_ACTION_TYPES: &[&str] = &[ - "linear_followup", - "none", - "signal_entry", - "social_candidate", - "social_post", - "upstream_impact", -]; +const UPSTREAM_REVIEW_ACTION_TYPES: &[&str] = + &["linear_followup", "none", "signal_entry", "social_candidate", "upstream_impact"]; const UPSTREAM_REVIEW_NEXT_STEPS: &[&str] = &["ai_review_required"]; const UPSTREAM_REVIEW_PRIORITIES: &[&str] = &["critical", "high", "low", "normal"]; const UPSTREAM_SOURCE_STATES: &[&str] = &["closed", "commit_only", "merged", "open"]; @@ -155,6 +149,7 @@ const ARTIFACT_KINDS: &[&str] = &[ "ledger_export", "release_delta", "signal", + "social_candidate", "social_post", "upstream_impact", ]; @@ -3234,6 +3229,7 @@ fn initialize_ledger(connection: &Connection) -> crate::prelude::Result<()> { 'analysis', 'signal', 'upstream_impact', + 'social_candidate', 'social_post', 'release_delta', 'archive_manifest', @@ -3263,7 +3259,7 @@ fn initialize_ledger(connection: &Connection) -> crate::prelude::Result<()> { ", )?; - migrate_artifact_link_social_kind(connection)?; + migrate_artifact_link_kinds(connection)?; connection.execute( " @@ -3277,7 +3273,7 @@ fn initialize_ledger(connection: &Connection) -> crate::prelude::Result<()> { Ok(()) } -fn migrate_artifact_link_social_kind(connection: &Connection) -> crate::prelude::Result<()> { +fn migrate_artifact_link_kinds(connection: &Connection) -> crate::prelude::Result<()> { let table_sql = connection .query_row( " @@ -3293,7 +3289,7 @@ fn migrate_artifact_link_social_kind(connection: &Connection) -> crate::prelude: return Ok(()); }; - if !table_sql.contains("social_draft") { + if table_sql.contains("social_candidate") && !table_sql.contains("social_draft") { return Ok(()); } @@ -3311,6 +3307,7 @@ fn migrate_artifact_link_social_kind(connection: &Connection) -> crate::prelude: 'analysis', 'signal', 'upstream_impact', + 'social_candidate', 'social_post', 'release_delta', 'archive_manifest', @@ -5196,6 +5193,9 @@ fn validate_social_candidate_source_refs(refs: Option<&Value>, errors: &mut Vec< .into(), ); } + if refs.get("urls").is_some_and(|urls| !is_https_string_array(urls)) { + errors.push("source_refs.urls must be a list of https URLs".into()); + } } fn validate_social_candidate_decision(decision: Option<&Value>, errors: &mut Vec) { @@ -5837,6 +5837,15 @@ mod tests { assert_errors(&candidate, ["source_refs must include upstream_reviews"]); } + #[test] + fn social_candidate_rejects_non_https_source_urls() { + let mut candidate = valid_social_candidate(); + + candidate["source_refs"]["urls"] = serde_json::json!(["http://example.test"]); + + assert_errors(&candidate, ["source_refs.urls must be a list of https URLs"]); + } + #[test] fn accepts_valid_upstream_impact_and_rejects_bad_angle() { let mut impact = valid_upstream_impact(); @@ -6088,7 +6097,7 @@ mod tests { .expect("schema version should be readable"); assert_eq!(artifact_kind, "social_post"); - assert_eq!(schema_version, "2"); + assert_eq!(schema_version, "3"); } #[test] @@ -6167,6 +6176,63 @@ mod tests { assert_eq!(artifact_kind, "social_post"); } + #[test] + fn ledger_artifact_link_records_social_candidate_after_schema_migration() { + let temp_dir = tempfile::tempdir().expect("temporary directory should be created"); + let db_path = temp_dir.path().join("radar.sqlite3"); + let social_candidate_path = temp_dir.path().join("candidate.json"); + let connection = + rusqlite::Connection::open(&db_path).expect("temporary ledger should open"); + + connection + .execute_batch( + " + CREATE TABLE artifact_link ( + repo TEXT NOT NULL, + subject_kind TEXT NOT NULL CHECK (subject_kind IN ('commit', 'pr')), + subject_id TEXT NOT NULL, + artifact_kind TEXT NOT NULL CHECK ( + artifact_kind IN ( + 'bundle', + 'analysis', + 'signal', + 'upstream_impact', + 'social_post', + 'release_delta', + 'archive_manifest', + 'ledger_export' + ) + ), + path TEXT NOT NULL, + sha256 TEXT NOT NULL, + size_bytes INTEGER NOT NULL, + created_at TEXT NOT NULL, + PRIMARY KEY (repo, subject_kind, subject_id, artifact_kind, path) + ); + ", + ) + .expect("legacy artifact link schema should be created"); + + drop(connection); + + fs::write(&social_candidate_path, r#"{"schema":"social_candidate/v1"}"#) + .expect("social candidate fixture should be written"); + radar::ledger_bootstrap(&RadarLedgerBootstrapRequest { db_path: db_path.clone() }) + .expect("ledger bootstrap should add social_candidate artifact kind"); + + let summary = radar::ledger_artifact_link(&RadarLedgerArtifactLinkRequest { + db_path: db_path.clone(), + repo: "openai/codex".into(), + subject_kind: "pr".into(), + subject_id: "22414".into(), + artifact_kind: "social_candidate".into(), + path: social_candidate_path, + }) + .expect("social candidate artifact link should be recorded"); + + assert_eq!(summary.get("artifact_links"), Some(&1)); + } + #[test] fn builds_pr_bundle_from_fixture_payloads() { let patch = format!("{} --config FEATURE_FLAG=1", "a".repeat(910)); @@ -6615,7 +6681,7 @@ mod tests { "made_with_ai": true, "image_template": "decodex_signal_card" }, - "media_refs": ["artifacts/social/x/images/openai-codex-pr-22414.png"] + "media_refs": ["https://x.com/decodexspace/status/1/photo/1"] }) } } diff --git a/artifacts/github/reviews/openai-codex-pr-25636.review.json b/artifacts/github/reviews/openai-codex-pr-25636.review.json index d53a4e0..e9cd16e 100644 --- a/artifacts/github/reviews/openai-codex-pr-25636.review.json +++ b/artifacts/github/reviews/openai-codex-pr-25636.review.json @@ -54,8 +54,8 @@ "reason": "The rename can affect Decodex Control Plane compatibility and should be tracked as an upstream impact." }, { - "type": "social_post", - "reason": "The rename is useful to explain publicly for operators following Codex MultiAgentV2 tooling changes." + "type": "social_candidate", + "reason": "The rename is useful to consider publicly for operators following Codex MultiAgentV2 tooling changes; Publisher should decide the terminal social_post outcome." }, { "type": "linear_followup", diff --git a/artifacts/social/README.md b/artifacts/social/README.md index e337af2..c652728 100644 --- a/artifacts/social/README.md +++ b/artifacts/social/README.md @@ -4,7 +4,11 @@ This directory stores checked-in Publisher artifacts for external social channel - `x/posts/` holds `social_post/v1` publication, block, skip, and failure records for X. -- `x/images/` holds generated media evidence when the image is committed. +- `x/images/` is legacy or explicit operator-approved sample storage only. Publisher + automation should not commit generated images by default; use X status/media URLs and + optional content hashes instead. -The governing contract is `docs/spec/social-publishing.md`. The primary publishing -account is `@decodexspace`; the controller account is `@hackink`. +Social candidates live under `artifacts/github/social-candidates/`; this directory holds +terminal Publisher outcomes. The governing publication contract is +`docs/spec/social-publishing.md`. The primary publishing account is `@decodexspace`; +the controller account is `@hackink`. diff --git a/dev/skills/README.md b/dev/skills/README.md index a8ff202..5546877 100644 --- a/dev/skills/README.md +++ b/dev/skills/README.md @@ -8,8 +8,8 @@ with the installable Decodex plugin under `plugins/decodex/`. ## Skill Map -Use these skills in order when turning upstream Codex activity into Decodex content or -follow-up work: +Use these skills as a pipeline when turning upstream Codex activity into Decodex +content or follow-up work: 1. `codex-upstream-triage`: read the deterministic upstream review queue or a selected source window and group commits by PR when possible. @@ -19,7 +19,9 @@ follow-up work: PRs, release-delta artifacts, and already-published Decodex signals. 4. `github-signal`: turn the reviewed GitHub bundle and analysis result into the `analysis_draft` JSON consumed by `decodex radar render-signal`. -5. `x-post-publisher`: turn evidence-backed Radar output into a low-frequency +5. `x-post-publisher`: consume social candidates whose + `decision.worthiness = "publish"` or explicit operator handoffs that name checked + Radar artifacts, then write a low-frequency `social_post/v1` publication, block, skip, or failure record for `@decodexspace`. 6. `rate-limit-reset-watch`: review today's `@thsottiaux` X posts with AI semantic judgment and refresh the homepage `reset_status/v1` artifact. @@ -27,6 +29,25 @@ follow-up work: Use only the skills needed for the current artifact. Do not publish a social post just because a signal exists. +## Pipeline Ownership + +Only the upstream analysis stage should read upstream Codex source for behavior claims: + +- `codex-upstream-triage` selects and groups source candidates. +- `codex-code-analysis` reads upstream PR, commit, file, or patch evidence and produces + the source-backed interpretation. + +Downstream skills are artifact consumers. `codex-release-analysis`, `github-signal`, +`x-post-publisher`, and `x-post-quality-system` should start from validated +`upstream_review/v1`, `upstream_impact/v1`, `signal_entry/v1`, `release_delta/v1`, +`social_candidate/v1`, or `analysis_draft` evidence. If that evidence is missing or too +weak, they must return `upstream_analysis_required` for missing source-analysis +evidence or preserve a `social_candidate/v1` with `decision.worthiness = "defer"` or +`"skip"` instead of doing ad hoc source analysis. + +Release and prerelease automations may use compare metadata to detect gaps, but filling +those gaps belongs back in the upstream analysis stage. + Default posture: track every upstream Codex commit as a possible evidence unit. Resolve commits back to PRs when possible, decide whether the change matters to Decodex Control Plane or the wider Codex community, and only then promote important, useful, or @@ -35,7 +56,9 @@ post. For upstream releases and prereleases, use `codex-release-analysis` as a rollup over the accumulated commit/PR analysis. Codex prerelease notes are often too sparse to -explain what changed by themselves. +explain what changed by themselves, but a new prerelease checkpoint can still produce a +cautious `social_candidate/v1` for a `release_pulse` intro or `watch_note` preview when +release metadata, compare metadata, and caveats create real reader value. Checked-in contracts for this workflow are `upstream_review_queue/v1`, `upstream_review/v1`, `github_change_bundle/v1`, `analysis_draft`, `signal_entry/v1`, diff --git a/dev/skills/codex-code-analysis/SKILL.md b/dev/skills/codex-code-analysis/SKILL.md index 7b1aaa6..0b68d1f 100644 --- a/dev/skills/codex-code-analysis/SKILL.md +++ b/dev/skills/codex-code-analysis/SKILL.md @@ -11,6 +11,11 @@ evidence into a defensible interpretation, not to rewrite release notes. This is a Decodex repository-development instruction surface, not an installable Decodex plugin skill. +This is the only repo-local skill that should read upstream Codex source for behavior, +compatibility, or Publisher claims during recurring Radar automation. Downstream +release, signal, and publishing skills consume this skill's reviewed artifacts instead +of redoing the source pass. + ## Read Before Analysis - `docs/spec/github-change-bundle.md` @@ -30,7 +35,7 @@ Decodex plugin skill. This skill may produce an `upstream_review/v1` when Codex automation is processing the continuous review queue. Keep ad hoc manual notes in-session unless they are promoted into `upstream_review/v1`, `analysis_draft`, `upstream_impact/v1`, or -`social_post/v1`. +`social_candidate/v1`. ## Analysis Loop @@ -92,7 +97,9 @@ Return an analysis note that can feed `github-signal`, `codex-release-analysis`, - Publisher angle, if any - confidence and caveats - recommended next artifact: `none`, `analysis_draft` through `github-signal`, - `upstream_impact/v1`, or `social_post/v1` + `upstream_impact/v1`, or `social_candidate/v1` +- downstream consumer gates: which artifacts are safe to consume and which claims still + require source review Keep the note shorter than the source patch. Explain the behavior path, not every changed file. diff --git a/dev/skills/codex-release-analysis/SKILL.md b/dev/skills/codex-release-analysis/SKILL.md index 6571336..391c7ac 100644 --- a/dev/skills/codex-release-analysis/SKILL.md +++ b/dev/skills/codex-release-analysis/SKILL.md @@ -5,37 +5,39 @@ description: Use when evaluating upstream Codex releases, prereleases, app updat # Decodex Codex Release Analysis -Use this skill when the source is release-shaped: GitHub releases, prerelease tags, -OpenAI developer changelogs, app update notes, or a release-focused social post. - -This is a Decodex repository-development instruction surface, not an installable -Decodex plugin skill. +Use this repo-local skill when GitHub releases, prerelease tags, changelogs, app update +notes, or release-focused social posts need interpretation from already-reviewed Radar +evidence. ## Read Before Analysis -- `docs/spec/release-delta.md` -- `docs/spec/signal-entry.md` -- `docs/spec/upstream-impact.md` -- `docs/spec/social-publishing.md` +- `docs/spec/release-delta.md`, `docs/spec/signal-entry.md`, + `docs/spec/upstream-impact.md`, `docs/spec/social-candidate.md`, and + `docs/spec/social-publishing.md` - `docs/runbook/local-github-signal-workflow.md` - `dev/skills/codex-upstream-triage/SKILL.md` -- `dev/skills/codex-code-analysis/SKILL.md` +- `dev/skills/codex-code-analysis/SKILL.md` only when explicitly routing missing + release-window gaps back to source analysis ## Inputs - Release tag, changelog URL, or app update URL -- Existing `release_delta/v1` artifact, when available -- Existing Decodex signals that may explain the release delta -- GitHub compare, commit, or PR evidence for any claim beyond the release headline +- Existing `release_delta/v1`, `upstream_review/v1`, `upstream_impact/v1`, and + `signal_entry/v1` artifacts +- GitHub compare metadata for gap detection, not as a substitute for source review + +This advisory pass does not replace deterministic `release_delta/v1` generation. -This skill is an advisory reasoning pass. It does not define a new checked-in -release-analysis artifact and does not replace deterministic `release_delta/v1` -generation. +## Source Analysis Boundary + +Do not perform fresh upstream source analysis. Use compare metadata only to find +unreviewed PR/commit gaps, then return `upstream_analysis_required` for +`codex-upstream-triage` and `codex-code-analysis`. Publish rollups only from +source-backed artifacts plus release/compare metadata. ## Release Reading Rules -- Treat release and prerelease tags as reporting checkpoints over the commit/PR stream, - not as a separate higher-priority intake lane. +- Treat release and prerelease tags as checkpoints over the commit/PR stream. - Treat release notes as discovery, not proof, when they are sparse. - Use GitHub compare data and PR mappings to explain what changed between stable and prerelease tags. @@ -43,8 +45,7 @@ generation. or PR evidence. - Do not imply a feature is broadly available when the source says alpha, beta, rollout, platform-gated, or config-gated. -- Do not write a release recap that only duplicates a release bot. Prefer a summary - built from accumulated Decodex signal, upstream-impact, and commit/PR analysis. +- Do not duplicate a release bot; add accumulated Decodex analysis or caveats. ## Release Rollup Path @@ -53,15 +54,14 @@ When the target is an OpenAI Codex release or prerelease: 1. Refresh or read `release_delta/v1`. 2. Select the top-level `stable_release` -> `prerelease` comparison unless the user asks for a specific tag pair. -3. Start from existing `signal_entry/v1`, `upstream_impact/v1`, and recent - commit/PR analyses that match the compare range. -4. Use `decodex radar backfill-release-range --dry-run` to find `compare.pr_numbers` - gaps that still need code analysis. -5. Group findings by reader value: useful now, important for Decodex Control Plane, - deprecated/removed behavior, and watch-only changes. -6. Publish release or prerelease X reporting only after the summary is grounded in - those historical analyses and passes the daily cap. -7. Refresh `release_delta/v1` after new signals are rendered so the homepage can map +3. Start from matching `signal_entry/v1`, `upstream_impact/v1`, and recent analyses. +4. Run `decodex radar backfill-release-range --dry-run` to find unreviewed gaps. +5. Group useful, Control Plane, deprecated/removed, and watch-only changes. +6. If material gaps remain, stop with `upstream_analysis_required` instead of filling + them inside this skill. +7. Write `social_candidate/v1` with `decision.worthiness = "publish"`, `"defer"`, or + `"skip"` when the checkpoint needs a durable Publisher decision. +8. Refresh `release_delta/v1` after new signals are rendered so the homepage can map the release window to tracked signals. ## Analysis Modes @@ -70,36 +70,35 @@ Use exactly one primary mode: | Mode | Use when | Output | | --- | --- | --- | -| `release_pulse` | The release headline is the story and evidence is thin. | Short awareness note or social post. | +| `release_pulse` | The release headline is the story. | Short awareness note or social post. | | `delta_explainer` | Compare commits map to existing signals or clear PRs. | Refresh existing `release_delta/v1` and summarize the evidence. | -| `operator_impact` | Release changes app-server, plugins, browser, MCP, permissions, sandbox, hooks, config, auth, or providers. | `upstream_impact/v1` plus possible follow-up issue. | +| `operator_impact` | Release changes app-server, plugins, browser, MCP, permissions, sandbox, hooks, config, auth, or providers. | `upstream_impact/v1` plus follow-up if needed. | | `watch_note` | The release is interesting but evidence is incomplete. | Watch note with caveats. | -For sparse Codex prereleases, prefer `delta_explainer`, `operator_impact`, or a -source-backed release rollup over `release_pulse`; the release version alone is rarely -the useful story. +## Prerelease Intro Path + +Do not treat sparse prerelease notes as automatic silence. For every new prerelease +checkpoint, choose one outcome: -## Style Lessons +- `release_pulse`: short candidate intro from public release and compare metadata. +- `watch_note`: a cautious preview when the checkpoint is interesting but release-window + source analysis is incomplete. +- `release_rollup`: a stronger post when existing upstream reviews, impacts, and signals + explain the useful changes. +- `defer` or `skip` candidate decision: when a post would only repeat a version tag or + would require unreviewed code claims. -- Release-bot style is useful for speed: version, three bullets, source link. -- Human analysis style is useful for value: what changes in a real workflow, why it - matters, what to try, and where the limit remains. -- Decodex should prefer the human-analysis shape whenever source evidence supports it. +The intro must name the tag, source, timing, and evidence gap. Do not invent feature +claims from the tag name, sparse body, or social style references. ## Output Return: -- release source and timestamp -- whether the release body is explanatory or sparse -- compare or PR evidence used -- matching Decodex signal slugs, if any -- chosen mode -- user-facing takeaway -- Control Plane impact, if any -- Publisher recommendation: no post, `release_pulse`, `practical_explainer`, - `operator_impact`, `release_rollup`, or `watch_note` +- source/timestamp, release-body quality, compare/PR evidence, matching signal slugs +- chosen mode, takeaway, Control Plane impact, and `social_candidate/v1` + `decision.worthiness` Promote durable conclusions into existing artifacts only: `upstream_impact/v1`, Codex-owned `analysis_draft` plus `decodex radar render-signal` output, refreshed -`release_delta/v1`, or `social_post/v1`. +`release_delta/v1`, `social_candidate/v1`, or terminal `social_post/v1`. diff --git a/dev/skills/codex-upstream-triage/SKILL.md b/dev/skills/codex-upstream-triage/SKILL.md index b18c2d7..72a43e1 100644 --- a/dev/skills/codex-upstream-triage/SKILL.md +++ b/dev/skills/codex-upstream-triage/SKILL.md @@ -96,7 +96,7 @@ Return a compact triage note with: - next skill to use - confidence limits -Do not draft `signal_entry/v1` or publish `social_post/v1` directly from this skill. -Do not treat deterministic queue hints as technical claims. The durable review layer is -`upstream_review/v1`; public and Control Plane artifacts are promotions from that -source-backed review. +Do not draft `signal_entry/v1`, `social_candidate/v1`, or `social_post/v1` directly +from this skill. Do not treat deterministic queue hints as technical claims. The durable +review layer is `upstream_review/v1`; public and Control Plane artifacts are promotions +from that source-backed review. diff --git a/dev/skills/github-signal/SKILL.md b/dev/skills/github-signal/SKILL.md index 6fc946a..ce34059 100644 --- a/dev/skills/github-signal/SKILL.md +++ b/dev/skills/github-signal/SKILL.md @@ -10,10 +10,9 @@ This is a Decodex repository-development instruction surface, not a complete user-facing plugin skill, and it must not be packaged with the installable Decodex plugin. -This skill does not replace the deterministic Radar CLI. It tells Codex how to read a -reviewed bundle and in-session code-analysis result, decide whether the change deserves -publication, and draft the analysis JSON that `decodex radar render-signal` renders -into a final `signal_entry/v1`. +This skill does not replace the deterministic Radar CLI or upstream source analysis. +It consumes a reviewed bundle plus source-backed analysis and drafts the JSON that +`decodex radar render-signal` renders into a final `signal_entry/v1`. ## Read before drafting @@ -28,8 +27,8 @@ into a final `signal_entry/v1`. ## Inputs - A normalized bundle JSON under `artifacts/github/bundles/` -- A code-analysis result from `dev/skills/codex-code-analysis/SKILL.md`, when the - behavior path is not already clear from the bundle +- A source-backed `upstream_review/v1` or code-analysis result from + `dev/skills/codex-code-analysis/SKILL.md` - An output path under `artifacts/github/analysis/` - Optional upstream impact output under `artifacts/github/impact/` @@ -45,6 +44,9 @@ into a final `signal_entry/v1`. ## Boundaries +- Do not perform fresh upstream source analysis here. If the behavior path or Control + Plane impact is not clear from the reviewed artifacts, return + `upstream_analysis_required`. - Treat the PR as the main narrative container. - Treat commits, files, and patch excerpts as evidence. - Do not summarize every commit as if it were independently important. @@ -57,71 +59,17 @@ into a final `signal_entry/v1`. - When a feature is gated by `config.toml`, prefer canonical user-facing toggles over raw patch constants or PR-local token strings. - When evidence is weak or the change is mostly internal cleanup, lower confidence or skip publication. -## Editorial decision ladder +## Editorial Gate -Do not collapse everything into one "worth trying" bucket. Make three separate decisions. +Use the signal-entry spec and local GitHub signal runbook for detailed editorial rules. +Make only these decisions here: -### 1. Signal-worthy at all +- signal-worthy: user-visible capability, behavior change, try path, or migration value +- `try_now`: concrete short-session try path plus observable expected effect +- homepage highlight: confirmed, concrete, and high reader value -Publish a signal only when the change crosses at least one of these bars: - -- It exposes a new user-facing capability. -- It changes user-visible behavior in a meaningful way. -- It gives users a concrete new path they can validate now. - -Do not publish purely for internal cleanup, invisible refactors, telemetry, plumbing, or groundwork unless the user-facing effect is already clear. - -### 2. Should this be `kind = "try_now"`? - -Use `kind = "try_now"` only when the answer to "should a reader actively go try this now?" is yes. - -Require all of these: - -- The try path is concrete, bounded, and realistic for a normal product reader. -- The expected effect is directly observable by that reader. -- The payoff is user-facing, not just implementation-facing. -- The change feels newly reachable now, not merely documented or exposed as metadata. - -Do not use `try_now` just because a command exists. Keep the signal as `capability` or `behavior_change` when the change is mainly informative, low-stakes, contributor-facing, operator-facing, or too niche to recommend broadly. - -### 3. Should this surface as a homepage highlight? - -Do not use a numeric score. Use a simple gate plus amplifier rule. - -Hard gates: all of these must be true. - -- `how_to_try` is present and concrete. -- `expected_effect` is present and concrete. -- `confidence = "confirmed"`. -- A normal prerelease reader can try it in one short session. -- The payoff is clear enough that the reader would care today, not just note it for later. - -Amplifier rule: at least one of these must also be true. - -- It unlocks a newly reachable workflow or product surface. -- It removes noticeable friction from a common workflow. -- It changes visible behavior or output in a way a user can directly confirm. - -Good shortcut: - -- If the reader can answer "I should go try this" after one sentence, it is probably highlight material. -- If the reader only thinks "good to know", it probably belongs in the full feed instead. - -Treat a signal as not homepage-highlight material when any of these are true: - -- It is mostly internal refactor, cleanup, telemetry, groundwork, or API surface bookkeeping. -- It is useful to know but not worth interrupting the homepage reading flow for. -- The try path is too indirect, too expensive, too environment-specific, or too admin-only. -- The expected effect is vague enough that the reader would not know whether it worked. -- It is a low-impact capability detail that belongs in the full feed, even if it includes a demo path. - -Editorial tie-breakers: - -- If several signals describe the same user journey, pick the clearest user payoff as the highlight and leave sibling details for the full feed. -- Prefer one obvious workflow win over multiple small implementation-adjacent deltas. -- `why_it_matters` should explain the user payoff, not restate the patch. -- `how_to_try` should stay short and runnable. -- `expected_effect` should describe success in reader terms. +Skip internal cleanup, telemetry, plumbing, groundwork, and weak evidence. Keep +`why_it_matters`, `how_to_try`, and `expected_effect` focused on reader value. ## Draft shape @@ -141,8 +89,8 @@ Write a JSON analysis draft with these fields: ## Workflow 1. Validate the bundle first. -2. Read `primary_pr.title`, `primary_pr.body`, `files`, `commits`, and the companion - in-session code-analysis result when one was produced. +2. Read the reviewed bundle metadata plus the source-backed upstream review or + code-analysis result. 3. Decide whether the change is signal-worthy. 4. Draft the `analysis_draft` JSON under `artifacts/github/analysis/`. 5. Draft or update an `upstream_impact/v1` artifact when the change affects Control Plane or diff --git a/dev/skills/x-post-publisher/SKILL.md b/dev/skills/x-post-publisher/SKILL.md index b5cb1ff..6352ec7 100644 --- a/dev/skills/x-post-publisher/SKILL.md +++ b/dev/skills/x-post-publisher/SKILL.md @@ -1,133 +1,72 @@ --- name: x-post-publisher -description: Use when turning Decodex Radar evidence, upstream-impact classifications, signal entries, release analysis, or verified browser style observations into an automated @decodexspace X post or a checked-in social_post/v1 blocked/skipped/failed record. +description: Use when publishing a checked `social_candidate/v1` or explicit operator handoff to @decodexspace, or writing the matching `social_post/v1` non-published record. --- # Decodex X Post Publisher -Use this skill after source evidence exists. Its job is to decide whether a candidate is -worth publishing, publish low-frequency high-value posts from `@decodexspace` through -Chrome when the account state is safe, and write the `social_post/v1` publication -record. +Use after a `social_candidate/v1` with `decision.worthiness = "publish"` exists, or +after an explicit operator handoff names checked Radar artifacts. This skill consumes +that candidate or handoff, optionally posts through Chrome as `@decodexspace`, and +always writes the terminal `social_post/v1` record. This is a Decodex repository-development instruction surface, not an installable Decodex plugin skill. -## Read Before Publishing +## Required Context - `docs/spec/social-publishing.md` -- `docs/spec/upstream-impact.md` +- `docs/spec/social-candidate.md` - `docs/runbook/social-publishing-workflow.md` - `dev/skills/x-post-quality-system/SKILL.md` -- `dev/skills/codex-release-analysis/SKILL.md` -- `dev/skills/codex-code-analysis/SKILL.md` -## Inputs +## Boundaries -- `signal_entry/v1`, `upstream_impact/v1`, `upstream_review/v1`, release-analysis - note, or checked source URLs -- Optional style observations from `@CodexReleases`, `@Codex_Changelog`, `@LLMJunky`, - or `@decodexspace` -- Target account: `decodexspace` -- Controller account for attribution and site links: `hackink` - -## Browser Boundary - -Use `@Chrome` for X publication only inside this low-frequency Publisher workflow. -Before composing, verify the logged-in account is `@decodexspace`. If account -verification, Chrome availability, X page structure, media upload, duplicate detection, -or final URL readback is unreliable, do not post. Write `status = "failed"` or -`status = "blocked"` with evidence instead. - -Treat Chrome tabs as scoped resources. After account verification, compose, upload, -and final URL readback are done, close or release all tabs opened for the workflow. -Keep a tab only as an explicit human handoff, such as login, CAPTCHA, account approval, -or an unfinished operator-controlled page, and record that handoff in the result. - -Style observations from X are not technical evidence. They can shape format and tone, -but every technical claim must point back to GitHub, changelog, signal, upstream-review, -or upstream-impact evidence. - -## Benchmark Patterns - -Use these as format patterns only: - -| Pattern | Good for | Decodex adaptation | -| --- | --- | --- | -| Release/update card | `release_pulse` or high-value `release_rollup` posts. | Product/version/theme headline, two or three reader-visible changes, source link, optional thread details. | -| High-density changelog | One-post summaries from a source changelog or release. | Headline, three high-signal bullets, source card; no extra commentary. | -| Release rollup | `release_rollup` posts after a release or prerelease. | Summarize what prior commit/PR analysis found: useful now, Control Plane impact, deprecations, and watch-only gaps. | -| Human workflow read | `practical_explainer` and `operator_impact`. | Start with the concrete workflow change, then explain why it matters and what caveat remains. | -| Watch note | Interesting but incomplete evidence. | Say what changed, why Radar is watching, and what evidence is still missing. | +- Do not read upstream Codex source, patches, or PR files here. +- Do not create fresh analysis. Consume checked artifacts and the candidate or operator + handoff only. +- Use Chrome only for this low-frequency publishing workflow. +- Before posting, verify the logged-in X account is `@decodexspace`. +- If account state, X compose/readback, upload, duplicate detection, or final URL + readback is unreliable, do not post; write `blocked` or `failed`. +- Treat X style observations as format input only. Technical claims must come from + GitHub, changelog, signal, upstream-review, upstream-impact, release-delta, or + candidate evidence. ## Publish Modes -Choose exactly one `mode` from `social_post/v1`: - -- `release_pulse`: short release-aware summary with source link. -- `release_rollup`: release or prerelease summary built from accumulated Radar - analysis. -- `practical_explainer`: concrete user workflow and expected result. -- `operator_impact`: Decodex Control Plane implication. -- `thread`: multi-post explanation when one post hides evidence or caveats. -- `watch_note`: cautious public note for incomplete evidence. - -`@decodexspace` should mostly use `practical_explainer`, `operator_impact`, and -source-backed `release_rollup`. Use `release_pulse` only when the release itself is the -useful alert. +Choose one mode: `release_pulse`, `release_rollup`, `practical_explainer`, +`operator_impact`, `thread`, or `watch_note`. Prefer `practical_explainer`, +`operator_impact`, and source-backed `release_rollup`. A prerelease can be a +`release_pulse` intro or `watch_note` only when tag, source, timing, compare metadata, +and caveats are explicit. ## Worthiness Gate Publish only when all are true: -- The post is in English. +- The input is either a `social_candidate/v1` with + `decision.worthiness = "publish"`, or an explicit operator handoff naming checked + Radar artifacts. +- The post is in English and passes `x-post-quality-system`. - The source evidence is enough for every technical claim. -- `dev/skills/x-post-quality-system/SKILL.md` passes the candidate as externally - valuable. -- The candidate answers in one screen: what changed, who can use or observe it, and what - source proves it. -- The item is `critical` or `high`, or it is a release/prerelease rollup with clear - reader value. -- The post is useful to Codex users, Decodex operators, or builders tracking the Codex - app-server ecosystem. +- The item is `critical` or `high`, or it is a release/prerelease rollup, intro, or + watch note with clear reader value. - The idempotency key has not already been published or blocked for the same source. -- The daily cap has not been reached. - -Skip low-value internal churn. Do not post just because a signal exists. In particular, -do not publish single-PR renames, trace-only compatibility notes, low-context operator -cautions, or Decodex-internal audit reminders unless they roll up into a broader -release/update story or concrete external operator decision. - -## Daily Cap - -The daily cap is 8 X posts for `@decodexspace`, counted by `Asia/Shanghai` calendar day -unless the operator supplies another timezone. +- The daily cap of 8 posts for `@decodexspace` in `Asia/Shanghai` is not reached. -If publishing the candidate would exceed the cap: - -- do not post -- write `social_post/v1` with `status = "blocked"` -- set `block.reason = "daily_cap_exceeded"` -- preserve candidate text, source refs, priority, worthiness reason, and daily counts -- report the block in the automation result so the operator can analyze why volume - exceeded the cap +Skip low-value internal churn. Do not publish a single-PR rename, trace-only note, +low-context operator caution, or internal audit reminder unless it rolls up into a +broader release/update story or concrete external operator decision. ## Image Generation -Generate an image for each published post only when useful media can pass -`dev/skills/x-post-quality-system/SKILL.md`. Use -`image_template = "decodex_signal_card"` as a visual system, not as a fixed reusable -asset. Start with the exact base prompt in `docs/spec/social-publishing.md`, then add -candidate-specific subject, source, visual metaphor, palette, and forbidden-elements -slots from the quality-system skill. - -Do not rely on the generated image for readable text. Render any title, source, date, -PR number, release tag, or mode with deterministic overlay tooling or keep it in the -post body. Never reuse prior live-test images, old generic signal-card assets, unrelated -abstract cards, or weak decorative filler. - -If no fresh candidate-specific image passes visual review, publish text-only only when -the post remains valuable with the source link card. Otherwise skip or fail closed. +Generate media only when a fresh candidate-specific image can pass quality review. +Never rely on generated readable text or reuse prior live-test, generic, or unrelated +media. Keep generated image files in `$CODEX_HOME/decodex/social-media/` or temporary +storage by default, not Git. After upload, record the X status URL, any `/photo/N` +readback URL, and a short prompt/hash note when useful. If text-only is still valuable +with the source link card, publish text-only; otherwise skip or fail closed. ## Claim Review @@ -139,7 +78,7 @@ Before publishing: - Avoid local paths, credentials, private issue details, or internal runtime state. - Keep each `text[]` item within the X length limit. -## Output +## Output Record Write `artifacts/social/x/posts//.json` with: @@ -151,9 +90,15 @@ Write `artifacts/social/x/posts//.json` with: - `source_refs`, `evidence_notes`, `claims`, and `decision` - `publication` when posted - `block`, `failure`, or `skip` when not posted +- X media URL or media caveat when media was used or skipped; do not commit generated + image files unless explicitly operator-approved + +If the daily cap would be exceeded, write `status = "blocked"` with +`block.reason = "daily_cap_exceeded"` and preserve candidate text, source refs, +priority, worthiness reason, and daily counts. Run: ```bash -decodex radar validate artifacts/social/x +decodex radar validate artifacts/github/social-candidates artifacts/social/x ``` diff --git a/dev/skills/x-post-quality-system/SKILL.md b/dev/skills/x-post-quality-system/SKILL.md index a1ddc1b..84c5985 100644 --- a/dev/skills/x-post-quality-system/SKILL.md +++ b/dev/skills/x-post-quality-system/SKILL.md @@ -1,16 +1,23 @@ --- name: x-post-quality-system -description: Use before Decodex X Publisher decides to publish or generate media. Defines the @decodexspace editorial bar, benchmark-derived post formats, and the decodex_signal_card visual system so automation rejects low-value content and weak images. +description: Use when Decodex X Publisher needs to decide whether an artifact-backed @decodexspace post or generated media is worth publishing. --- # Decodex X Post Quality System -Use this skill before `x-post-publisher` decides that a candidate is publishable. -It raises the bar from source-backed correctness to public-reader value and media quality. +Use this skill before `x-post-publisher` decides that a `social_candidate/v1` or +explicit operator handoff is worth publishing. It raises the bar from source-backed +correctness to public-reader value and media quality. This is a repo-local skill for the `@decodexspace` automation. It is not a generic social-media style guide. +This skill evaluates value and media quality only. It must not read upstream Codex +source or invent missing technical evidence; keep weak candidates as +`decision.worthiness = "defer"` or `"skip"` before publishing, or write terminal +`skipped` or `blocked` `social_post/v1` records when the Publisher flow has already +started. + ## Benchmarks Studied The current standard comes from live-readback sampling on 2026-06-03: @@ -46,68 +53,23 @@ external operator decision. ## Good Post Shapes -Prefer one of these shapes. - -### Release Or Update Card - -Use when a release, prerelease, app update, or changelog entry is the story. - -Pattern: - -```text -Codex update: - -What changed: -- -- -- - -Source: -``` - -Use a short thread only when the main post already has enough value and details would not -fit cleanly. Follow-up posts should be `details`, `fixed`, `availability`, or `source`; -do not fragment weak material into a thread. - -### High-Density Changelog - -Use when the value is a concise summary of a known source. - -Pattern: - -```text -Codex is out. +Prefer concise release/update cards, dense changelog summaries, practical workflow +reads, prerelease intros, or concrete operator decisions. A good prerelease intro names +the tag/source/timing, says what is known, and states what Radar still needs to analyze; +it should feel more useful than a bare release bot without inventing details. Use +`operator_impact` only when the public action is external and concrete, such as enabling +or avoiding a provider/config/path, updating an integration assumption, planning a +rollout, or watching a beta/availability boundary. Do not turn a Decodex-internal audit +reminder into an X post. -- -- -- - -Changelog: -``` - -This shape should be dense and source-led. Do not add commentary that hides the actual -change. - -### Operator Decision - -Use `operator_impact` sparingly. It must read as an external operator decision, not a -Decodex maintenance note. - -Good operator-impact posts name a concrete action: - -- enable or avoid a provider/config/path -- update an integration assumption -- plan a rollout or migration -- watch a beta/availability boundary - -Do not publish an operator-impact post if the action is only "Decodex should audit this -internally." +Use threads only when the first post is valuable alone. Follow-ups should be focused +buckets such as highlights, fixed, added, security, availability, caveats, or source; +do not split a weak candidate into a thread to make it look substantial. ## Visual System -`decodex_signal_card` is a visual system, not a fixed image. Each publishable candidate -should get a fresh candidate-specific image when media is useful, but the image must share -the same visual grammar. +`decodex_signal_card` is a visual system, not a fixed image. Publish media only when a +fresh candidate-specific image is useful; the image must share the same visual grammar. Required shared elements: @@ -139,28 +101,19 @@ If no fresh, candidate-specific, quality-checked media exists, prefer text-only source link card when the post still has enough standalone value. Otherwise skip or fail closed. -## Image Prompt Contract - -Start from the `docs/spec/social-publishing.md` base prompt, then add source-specific -slots: +Generated image files are temporary Publisher resources. Store them in +`$CODEX_HOME/decodex/social-media/` or temporary storage by default, not Git. The durable +repository record should keep the X status/media URL, prompt summary or content hash +when useful, and any media caveat. -- `subject`: the concrete release/update/workflow being explained -- `source`: GitHub release, OpenAI changelog, PR, or signal artifact -- `visual_metaphor`: release card, source card, UI/workflow preview, or operator diagram -- `palette`: near-black or off-white with restrained magenta, lime, and blue accents -- `forbidden`: generic abstract art, long text, unreadable labels, people, mascots, - decorative blobs, unrelated UI - -Before upload, perform a visual quality check. The image must pass all: - -- It is specific to this candidate. -- It is visually consistent with the Decodex system. -- It is not ugly, noisy, generic, or off-brand. -- It still makes sense if the post text is read first. -- It does not rely on AI-rendered readable text. +## Image Prompt Contract -Record the prompt, media path, and quality-check outcome in `social_post/v1` evidence -notes or caveats. +Start from the `docs/spec/social-publishing.md` base prompt, then add `subject`, +`source`, `visual_metaphor`, `palette`, and `forbidden` slots. Before upload, verify the +image is candidate-specific, visually consistent, not generic or off-brand, useful with +the post text, and independent of AI-rendered readable text. Record the prompt summary, +content hash or final X media URL, and quality-check outcome in `social_post/v1` +evidence notes or caveats. ## Failure Rules diff --git a/docs/index.md b/docs/index.md index a47bef8..50f852b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -81,8 +81,9 @@ The split below is by question type, not by human-versus-agent audience. - Keep the public site static by default. `site/` consumes checked-in content and generated JSON; it must not depend on a live Decodex daemon unless a later decision changes that boundary. -- Keep social publishing static-first as well. Publication, block, skip, and failure - outcomes must be checked-in `social_post/v1` records. +- Keep social publishing static-first as well. Candidate handoffs must be checked-in + `social_candidate/v1` records, and publication, block, skip, and failure outcomes + must be checked-in `social_post/v1` records. - Start each document with a short routing header that says what the document is for, when to read it, and what it does not cover. - Keep links explicit and stable. diff --git a/docs/reference/workspace-layout.md b/docs/reference/workspace-layout.md index 750397c..47d854f 100644 --- a/docs/reference/workspace-layout.md +++ b/docs/reference/workspace-layout.md @@ -96,9 +96,11 @@ under `artifacts/archive/index/`. `artifacts/github/impact/` may hold `upstream_impact/v1` classifications when an upstream Codex change has public-signal, Control Plane, or Publisher implications. `artifacts/github/review-queue/` may hold the latest deterministic review queue. -`artifacts/social/` may hold `social_post/v1` published, blocked, failed, or skipped -records for external publication. Both remain checked-in artifacts; neither turns the -public site into a live service. +`artifacts/github/social-candidates/` may hold `social_candidate/v1` pre-publication +handoffs. `artifacts/social/` holds `social_post/v1` published, blocked, failed, or +skipped records for external publication. Generated media files are not checked-in by +default; records should point to X status/media URLs or optional content hashes instead. +These remain checked-in artifacts; none turns the public site into a live service. ## Installable Codex surface diff --git a/docs/runbook/local-github-signal-workflow.md b/docs/runbook/local-github-signal-workflow.md index 443b5bc..1725cb8 100644 --- a/docs/runbook/local-github-signal-workflow.md +++ b/docs/runbook/local-github-signal-workflow.md @@ -142,7 +142,7 @@ Repo-local editorial instruction entrypoint: These entrypoints are for Decodex repository development only. They are incomplete as general user-facing skills and must not be packaged with the installable Decodex plugin. Today only `github_change_bundle/v1`, `analysis_draft`, `signal_entry/v1`, -`upstream_impact/v1`, `release_delta/v1`, and `social_post/v1` are durable +`upstream_impact/v1`, `release_delta/v1`, `social_candidate/v1`, and `social_post/v1` are durable content contracts for this workflow. Automated sync entrypoint: diff --git a/docs/runbook/social-publishing-workflow.md b/docs/runbook/social-publishing-workflow.md index e0e13cf..bd3dfb0 100644 --- a/docs/runbook/social-publishing-workflow.md +++ b/docs/runbook/social-publishing-workflow.md @@ -11,9 +11,11 @@ Read this when: Inputs: - Source evidence from GitHub, checked-in signal entries, upstream reviews, - upstream-impact records, release-delta artifacts, or verified browser observations. + upstream-impact records, release-delta artifacts, social candidates, or verified + browser observations. - The governing schemas: - [`../spec/upstream-impact.md`](../spec/upstream-impact.md) + - [`../spec/social-candidate.md`](../spec/social-candidate.md) - [`../spec/social-publishing.md`](../spec/social-publishing.md) - [`../spec/signal-entry.md`](../spec/signal-entry.md) @@ -29,8 +31,11 @@ Depends on: Outputs: - An optional `upstream_impact/v1` artifact under `artifacts/github/impact/`. +- A `social_candidate/v1` record under `artifacts/github/social-candidates/` when + analysis needs a durable Publisher handoff or pre-publication decision. - A `social_post/v1` record under `artifacts/social/x/posts//`. -- Optional generated media under `artifacts/social/x/images/`. +- Optional local generated media under `$CODEX_HOME/decodex/social-media/`; generated + media files are not committed by default. ## Style Benchmarks @@ -40,29 +45,55 @@ for technical claims. | Account | Useful pattern | Decodex stance | | --- | --- | --- | | `@Codex_Changelog` | Fast release-aware bullets with a changelog link. | Useful for `release_pulse`, but Decodex should not become a duplicate release bot. | +| `@CodexReleases` | Version/update cards and short release threads. | Useful for timely release or prerelease intros, but Decodex should add evidence, caveats, and operator framing instead of only mirroring a tag. | | `@LLMJunky` | Practical user interpretation: how a feature changes real workflows, what is worth trying, and where limits remain. | Prefer this style when Radar evidence can support the claim quickly. | | `@decodexspace` | Low-frequency automated publication channel. | Establish a voice around evidence-backed Codex intelligence and Decodex operator impact. | +Live Chrome readback on 2026-06-04 confirmed two useful benchmark shapes: + +- `@Codex_Changelog` works as a single-card pattern: product/version headline, three + dense bullets, and a source link. Use this only when the checkpoint itself is the + reader value. +- `@CodexReleases` works as a thread pattern: lead card with highlights, focused + follow-ups for fixed/added/availability/security areas, and a source tail. Use this + when the release needs structure, but keep Decodex-specific caveats and evidence in + the lead instead of burying them in the thread. + ## Workflow 1. Start from source evidence. - Prefer a source-backed `upstream_review/v1`, merged PR bundle, release-delta compare entry, already-rendered `signal_entry/v1`, or `upstream_impact/v1`. - Do not start from social engagement alone. + - Publisher automation is an artifact consumer. It must not perform fresh upstream + Codex source analysis; if the checked artifacts do not support the claim, keep the + candidate at `decision.worthiness = "defer"` or `"skip"`, or write a terminal + `skipped` or `blocked` `social_post/v1` when the Publisher flow has already + started. 2. Classify upstream impact. - - Write or update `artifacts/github/impact/.json` when the change may affect - Control Plane or Publisher. + - Prefer existing `upstream_impact/v1`. If it is missing and the source-backed + review already proves the Control Plane or Publisher implication, write or update + `artifacts/github/impact/.json`. + - If impact depends on unreviewed code or patch evidence, route back to the upstream + analysis stage instead of resolving it inside Publisher. - Use `public_signal_decision`, `control_plane_impact`, and `publisher_angle` from [`../spec/upstream-impact.md`](../spec/upstream-impact.md). -3. Decide whether to publish. +3. Decide whether to create or consume a candidate. + - Release checkpoint automation should write `social_candidate/v1` with + `decision.worthiness = "publish"`, `"defer"`, or `"skip"`. + - General Publisher automation should consume only candidates whose + `decision.worthiness = "publish"`. It must not turn `defer` or `skip` decisions + into posts. + +4. Decide whether to publish. - Publish only when the change has a clear `release_pulse`, `practical_explainer`, `release_rollup`, `operator_impact`, or valuable `watch_note` angle. - Skip when the change is internal cleanup, too weakly sourced, too private, too vague, or not useful enough for a reader. -4. Check idempotency and daily cap. +5. Check idempotency and daily cap. - Build a stable idempotency key from account, source, mode, and release checkpoint when applicable. - Count already-published `@decodexspace` records for the cap day. @@ -70,42 +101,55 @@ for technical claims. - If the candidate would exceed 8 posts, do not post. Write `status = "blocked"` with `block.reason = "daily_cap_exceeded"`. -5. Generate media. +6. Prepare media only when useful. - Use the `decodex_signal_card` image template in [`../spec/social-publishing.md`](../spec/social-publishing.md). - Do not rely on AI-generated text in the image. - - Attach media unless the record explains why media was skipped. + - Keep generated files in the local media cache or temporary storage, not Git. + - It is acceptable to publish text-only when the post is useful with the source link + card. -6. Publish through Chrome. +7. Publish through Chrome. - Verify Chrome is logged in as `@decodexspace`. - Compose the English post or thread. - - Attach generated media when present. + - Attach generated media when it is useful and available. - Fail closed if account verification, duplicate detection, media upload, or final URL readback is unreliable. - Close or release Chrome tabs before the automation ends. Keep a tab only when it is an explicit human handoff such as login, CAPTCHA, or account approval. -7. Write the publication record. +8. Write the publication record. - Use `schema = "social_post/v1"`. - Use `target_account = "decodexspace"` and `controller_account = "hackink"`. - Set `status = "published"`, `blocked`, `failed`, or `skipped`. - Preserve source refs, evidence notes, claims, decision data, and publication URLs when available. + - For media, preserve the X status URL and any `/photo/N` readback URL. Do not add a + generated image file to Git unless the operator explicitly asks for a permanent + sample. -8. Validate. +9. Validate. - Run: ```bash -decodex radar validate artifacts/social/x +decodex radar validate artifacts/github/social-candidates artifacts/social/x ``` ## Mode Guidance +For every new prerelease checkpoint, choose one outcome instead of silently skipping: +`release_pulse`, `watch_note`, `release_rollup`, or a `social_candidate/v1` +`decision.worthiness = "defer"` or `"skip"` with a durable reason. A prerelease intro +is useful when it names the tag, channel, published time, source, and what Decodex is +watching, while clearly avoiding claims that require unreviewed code or PR evidence. + Use `release_pulse` when: - the release note itself is the story - the post is mainly fast awareness - the change does not yet justify a deeper Decodex angle +- a new prerelease is worth introducing from public release metadata, compare metadata, + and explicit caveats, even before a full release rollup exists Use `release_rollup` when: @@ -133,6 +177,8 @@ Use `watch_note` when: - the change is interesting but evidence is incomplete - rollout or platform status is unclear - a strong recommendation would overclaim +- the prerelease checkpoint is visible but Radar still needs upstream analysis before + it can describe behavior changes ## Guardrails diff --git a/docs/spec/radar-artifact-retention.md b/docs/spec/radar-artifact-retention.md index 9c1406d..98d0bd5 100644 --- a/docs/spec/radar-artifact-retention.md +++ b/docs/spec/radar-artifact-retention.md @@ -59,11 +59,16 @@ Keep these artifacts in Git unless a separate content cleanup explicitly removes - the current homepage `release_delta/v1` artifact - the latest `upstream_review_queue/v1` artifact under `artifacts/github/review-queue/` - `upstream_impact/v1` records that affect Decodex Control Plane or Publisher follow-up +- `social_candidate/v1` records that preserve `publish`, `defer`, or `skip` Publisher + intake decisions - `social_post/v1` records, including daily-cap blocks - archive manifests under `artifacts/archive/index/` -Generated social images may be archived after the same 21-day hot window when the -paired `social_post/v1` record keeps enough metadata to recover or regenerate them. +Generated social images are not warm curated artifacts and should not be committed by +automation by default. Keep them in a local media cache when visual QA or debugging +needs them. If an operator explicitly commits a sample image, treat it as a hot artifact +and archive or remove it after the same 21-day window when the paired `social_post/v1` +record keeps enough URL/hash/prompt metadata to audit the publication. ## Cold archive destination diff --git a/docs/spec/social-publishing.md b/docs/spec/social-publishing.md index 8b7b3a6..c219ac4 100644 --- a/docs/spec/social-publishing.md +++ b/docs/spec/social-publishing.md @@ -13,6 +13,7 @@ Read this when: Not this document: - The upstream GitHub bundle schema. Read [`github-change-bundle.md`](./github-change-bundle.md). - The public site signal-entry schema. Read [`signal-entry.md`](./signal-entry.md). +- The pre-publication handoff candidate. Read [`social-candidate.md`](./social-candidate.md). - The social publishing procedure. Read [`../runbook/social-publishing-workflow.md`](../runbook/social-publishing-workflow.md). @@ -21,7 +22,7 @@ Defines: - Allowed post modes for Decodex Publisher. - The automated Chrome publishing boundary. - The daily cap and blocked-publication ledger rule. -- The generated-image style contract. +- The generated-media publishing and retention boundary. ## Artifact Identity @@ -32,11 +33,15 @@ The canonical schema identifier is: Recommended checked-in locations: - `artifacts/social/x/posts//.json` -- `artifacts/social/x/images/.png` -`social_post/v1` is a publication record, not a review-only draft. The record is -written whether automation publishes the post, skips it, fails safely, or blocks it -because the daily cap has already been reached. +`social_post/v1` is a publication record, not a review-only draft or pre-publication +candidate. Use `social_candidate/v1` for handoff decisions before Publisher evaluates +account state, idempotency, daily cap, media, and final publication. + +Generated media files are not default Git artifacts. Store successful publication +facts in Git as small JSON records. Store generated image files in a local persistent +media cache, or discard them after upload, unless an operator explicitly asks to commit +an exact sample. ## Required Fields @@ -63,7 +68,8 @@ Optional fields: - `failure`: required when `status = "failed"`. - `skip`: required when `status = "skipped"`. - `caveats`: rollout limits, uncertainty, platform limits, or version gates. -- `media_refs`: checked-in or locally generated assets used by the post. +- `media_refs`: optional X media readback URLs, external media pointers, content + hashes, or explicitly operator-approved checked-in sample paths. ## Post Modes @@ -82,6 +88,11 @@ Use exactly one `mode` value: evidence-backed `release_rollup`. `release_pulse` is allowed only when the release itself is the useful alert. +For prerelease introductions, do not add a new mode. Use `release_pulse` when the +source-backed value is a timely prerelease alert, `watch_note` when the checkpoint is +worth tracking but release-window analysis is incomplete, and `release_rollup` only when +accumulated upstream reviews explain the useful changes. + ## Claim Rules Each `claims[]` entry must include: @@ -128,7 +139,7 @@ material needed for post-run analysis: - mode and priority - AI worthiness reason - candidate text -- generated image prompt or intended media refs, if any +- intended media pointer or media caveat, if any - `daily_count_before` - `daily_limit` @@ -160,8 +171,8 @@ login, CAPTCHA, account approval, or a page that still requires operator input. ## Generated Image Contract -Every published X post should include a generated image unless the publication record -explains why media was skipped. +Generated media is optional. Use it only when it adds reader value beyond the text and +source link card. Do not create or commit an image just to satisfy a default. Use the stable image template id: @@ -181,12 +192,40 @@ negative space for deterministic overlay text. Do not render long text in the im The AI image must not be trusted for text rendering. Render title, PR/tag, mode, and source labels with deterministic overlay tooling or keep them in the post text. +When generated media is used, prefer this retention model: + +- X is the durable public media host after publication. +- Git stores only the `social_post/v1` record, final X status URL, optional `/photo/N` + readback URL, source refs, idempotency key, and media caveats. +- A local persistent media cache may store the generated image, prompt, content hash, + and upload/readback notes for debugging or visual QA. +- Git should not store generated image files unless an operator explicitly requests a + permanent sample. + +Recommended local media-cache layout: + +- `$CODEX_HOME/decodex/social-media/x///image.png` +- `$CODEX_HOME/decodex/social-media/x///manifest.json` + +The manifest should stay local and may include prompt summary, generator, dimensions, +file size, sha256, X status URL, X media URL, and cleanup eligibility. Automation should +prune old cache entries according to operator policy; the cache is not source control. + ## Release Checkpoints Release and prerelease publishing is separate from continuous six-hour Radar review. Release checkpoint automation may poll upstream releases more frequently than the commit review loop, but it must publish only when a new release or prerelease checkpoint -appears and enough accumulated review evidence exists. +appears and enough evidence exists for the selected mode. Rollups must use prior `upstream_review/v1`, `upstream_impact/v1`, `signal_entry/v1`, and compare evidence. Sparse Codex prerelease bodies are not sufficient proof. +However, a prerelease intro does not need to pretend to be a full rollup: it may publish +a cautious `release_pulse` or `watch_note` from public release metadata, compare +metadata, and a clear caveat about what Radar has not analyzed yet. If the only fact is +the tag name with no reader value, automation should write a `social_candidate/v1` with +`decision.worthiness = "defer"` or `"skip"` instead of posting. + +Release checkpoint automation should normally write `social_candidate/v1` first. X +Publisher consumes only candidates whose `decision.worthiness = "publish"` and writes +the terminal `social_post/v1` record. diff --git a/docs/spec/upstream-impact.md b/docs/spec/upstream-impact.md index 31a8e79..49f2eff 100644 --- a/docs/spec/upstream-impact.md +++ b/docs/spec/upstream-impact.md @@ -56,7 +56,8 @@ Recommended checked-in location: Optional fields: - `candidate_followups`: bounded engineering or research follow-up suggestions. -- `social_notes`: notes useful to a later `social_post/v1`. +- `social_notes`: notes useful to a later `social_candidate/v1` or terminal + `social_post/v1`. - `caveats`: uncertainty, version gating, platform limits, or rollout limits. ## Control Plane impact ladder @@ -110,9 +111,7 @@ summary. - It should normally consume a source-backed `upstream_review/v1` conclusion when the change came from continuous Radar. - It may support a `signal_entry/v1`. -- It may support a `social_post/v1`. -- It may support a `social_candidate/v1` when upstream source analysis identifies a - public Publisher opportunity but does not publish. +- It may support a `social_candidate/v1` or terminal `social_post/v1`. - It may justify a later Linear issue or implementation brief. It does not replace any of those artifacts. diff --git a/docs/spec/upstream-review.md b/docs/spec/upstream-review.md index 19621fd..faf753b 100644 --- a/docs/spec/upstream-review.md +++ b/docs/spec/upstream-review.md @@ -59,7 +59,7 @@ Recommended checked-in location: - `artifacts/github/reviews/.review.json` Review artifacts are hot Radar artifacts unless they are promoted into -`upstream_impact/v1`, `signal_entry/v1`, `social_post/v1`, or a Linear follow-up. +`upstream_impact/v1`, `signal_entry/v1`, `social_candidate/v1`, or a Linear follow-up. Apply the 21-day hot-window rule from [`radar-artifact-retention.md`](./radar-artifact-retention.md). ## Queue requirements @@ -112,7 +112,7 @@ or public value. - confidence: `confirmed`, `likely`, or `weak` - source-backed evidence notes - next actions, each mapped to `none`, `upstream_impact`, `signal_entry`, - `social_candidate`, `social_post`, or `linear_followup` + `social_candidate`, or `linear_followup` AI review must read enough source evidence to explain behavior. A PR title, release title, or deterministic queue hint is not enough for a confirmed claim. @@ -134,10 +134,7 @@ Promote an upstream review into: - `signal_entry/v1` when it is community-ready and has user-visible capability, behavior, try path, or migration value. - `social_candidate/v1` when there is a clear public angle and source links are - available, but the upstream review automation must not write a publication record. -- `social_post/v1` when there is a clear public angle and source links are - available and the Publisher workflow is actually publishing, blocking, skipping, or - recording failure. + available. Publisher later decides whether to write terminal `social_post/v1`. - a Linear issue when Decodex should adopt, guard, migrate, or investigate the change. Do not promote low-value internal churn into public artifacts. Keep it traceable in the @@ -150,4 +147,4 @@ They may trigger a gap scan, but they must not replace commit and PR evidence. This matters most for Codex prereleases because prerelease bodies may be sparse or empty. Rollups should combine prior reviews, impact artifacts, public signals, and compare -metadata before producing a public summary or X post. +metadata before producing a social candidate or X post. diff --git a/scripts/github/README.md b/scripts/github/README.md index 017db04..fe35755 100644 --- a/scripts/github/README.md +++ b/scripts/github/README.md @@ -42,11 +42,12 @@ Rust CLI entrypoints: Current checked contracts: - `analysis_draft.schema.json` is the Codex AI helper output schema. -- `upstream_review_queue/v1` is validated by `decodex radar validate`. -- `upstream_review.schema.json` is validated by `decodex radar validate`. -- `release_delta/v1` is validated by `decodex radar validate`. -- `upstream_impact.schema.json` is validated by `decodex radar validate`. -- `social_post.schema.json` is validated by `decodex radar validate`. +- `upstream_review_queue/v1` artifacts are validated by `decodex radar validate`. +- `upstream_review/v1` artifacts are validated by `decodex radar validate`. +- `release_delta/v1` artifacts are validated by `decodex radar validate`. +- `upstream_impact/v1` artifacts are validated by `decodex radar validate`. +- `social_candidate/v1` artifacts are validated by `decodex radar validate`. +- `social_post/v1` artifacts are validated by `decodex radar validate`. Contract ownership: @@ -54,6 +55,7 @@ Contract ownership: - upstream review queue and AI review boundary: `docs/spec/upstream-review.md` - output signal shape: `docs/spec/signal-entry.md` - upstream impact shape: `docs/spec/upstream-impact.md` +- social candidate shape: `docs/spec/social-candidate.md` - social publication shape: `docs/spec/social-publishing.md` Example flow: @@ -120,7 +122,8 @@ decodex radar backfill-release-range \ GitHub Actions may refresh upstream queues, release deltas, and validation through `decodex radar ...`. Codex automation owns AI review of queued subjects and may then promote source-backed conclusions into `upstream_impact/v1`, `analysis_draft`, -`decodex radar render-signal` output, or `social_post/v1`. +`decodex radar render-signal` output, or `social_candidate/v1`; Publisher automation +writes terminal `social_post/v1` records. Do not wire `run_codex_analysis.py` into GitHub Actions. Actions must not pass `--allow-ai-analysis-boundary` or set `DECODEX_ALLOW_CODEX_ANALYSIS`; that @@ -129,9 +132,9 @@ recovery runs that still keep bundle validation and `analysis_draft` schema vali inside the helper. Repo-local skills under `dev/skills/` are reasoning instructions for the Codex -analysis step and for manual Radar/Publisher work. They do not introduce extra -intermediate artifact schemas unless the conclusion is promoted into one of the -checked-in contracts listed above. +analysis step and for manual Radar/Publisher work. Pre-publication social decisions use +the checked-in `social_candidate/v1` contract; terminal publication, block, skip, and +failure outcomes use `social_post/v1`. Raw bundles and analysis drafts are retained in Git for a 21-day hot window. Archive older raw batches as dedicated `radar-archive-*` GitHub Release assets and commit only diff --git a/scripts/github/contracts.py b/scripts/github/contracts.py index 844ca5b..15a1330 100644 --- a/scripts/github/contracts.py +++ b/scripts/github/contracts.py @@ -15,6 +15,7 @@ RELEASE_DELTA_SCHEMA = "release_delta/v1" UPSTREAM_REVIEW_QUEUE_SCHEMA = "upstream_review_queue/v1" UPSTREAM_REVIEW_SCHEMA = "upstream_review/v1" +SOCIAL_CANDIDATE_SCHEMA = "social_candidate/v1" SOCIAL_POST_SCHEMA = "social_post/v1" ANALYSIS_MODES = {"pr_first", "commit_only"} SIGNAL_KINDS = {"capability", "behavior_change", "try_now"} @@ -29,7 +30,7 @@ "none", "upstream_impact", "signal_entry", - "social_post", + "social_candidate", "linear_followup", } SOCIAL_POST_MODES = { @@ -631,6 +632,97 @@ def validate_upstream_review(entry: dict[str, Any]) -> ValidationResult: return ValidationResult(ok=not errors, errors=errors) +def validate_social_candidate(entry: dict[str, Any]) -> ValidationResult: + errors: list[str] = [] + + if entry.get("schema") != SOCIAL_CANDIDATE_SCHEMA: + errors.append(f"schema must be {SOCIAL_CANDIDATE_SCHEMA}") + + for field in ("slug", "repo", "audience"): + if not isinstance(entry.get(field), str) or not entry[field]: + errors.append(f"{field} must be a non-empty string") + + if isinstance(entry.get("repo"), str) and "/" not in entry["repo"]: + errors.append("repo must be owner/name") + if entry.get("channel") != "x": + errors.append("channel must be x") + if entry.get("target_account") != "decodexspace": + errors.append("target_account must be decodexspace") + if entry.get("mode") not in SOCIAL_POST_MODES: + errors.append(f"mode must be one of {sorted(SOCIAL_POST_MODES)}") + if entry.get("priority") not in SOCIAL_POST_PRIORITIES: + errors.append(f"priority must be one of {sorted(SOCIAL_POST_PRIORITIES)}") + + candidate_text = entry.get("candidate_text") + if ( + not isinstance(candidate_text, list) + or not candidate_text + or not all(isinstance(item, str) and 0 < len(item) <= 280 for item in candidate_text) + ): + errors.append("candidate_text must be a non-empty list of X-sized strings") + + refs = entry.get("source_refs") + if not isinstance(refs, dict): + errors.append("source_refs must be an object") + else: + present = [ + name + for name in ("signals", "upstream_impacts", "upstream_reviews", "release_deltas", "urls") + if isinstance(refs.get(name), list) and refs[name] + ] + if not present: + errors.append( + "source_refs must include signals, upstream_impacts, upstream_reviews, release_deltas, or urls" + ) + urls = refs.get("urls", []) + if urls and ( + not isinstance(urls, list) + or not all(isinstance(url, str) and url.startswith("https://") for url in urls) + ): + errors.append("source_refs.urls must be a list of https URLs") + + for list_field in ("evidence_notes", "claims"): + values = entry.get(list_field) + if not isinstance(values, list) or not values: + errors.append(f"{list_field} must be a non-empty list") + + claims = entry.get("claims") + if isinstance(claims, list): + for index, claim in enumerate(claims): + if not isinstance(claim, dict): + errors.append(f"claims[{index}] must be an object") + continue + for field in ("text", "evidence"): + if not isinstance(claim.get(field), str) or not claim[field]: + errors.append(f"claims[{index}].{field} must be a non-empty string") + if claim.get("confidence") not in SIGNAL_CONFIDENCE: + errors.append(f"claims[{index}].confidence must be one of {sorted(SIGNAL_CONFIDENCE)}") + + decision = entry.get("decision") + if not isinstance(decision, dict): + errors.append("decision must be an object") + else: + if decision.get("worthiness") not in {"publish", "defer", "skip"}: + errors.append("decision.worthiness must be one of ['defer', 'publish', 'skip']") + for field in ("idempotency_key", "reason"): + if not isinstance(decision.get(field), str) or not decision[field]: + errors.append(f"decision.{field} must be a non-empty string") + + caveats = entry.get("caveats", []) + if caveats is not None and ( + not isinstance(caveats, list) or not all(isinstance(item, str) and item for item in caveats) + ): + errors.append("caveats must be a list of non-empty strings when present") + + next_steps = entry.get("next_steps", []) + if next_steps is not None and ( + not isinstance(next_steps, list) or not all(isinstance(item, str) and item for item in next_steps) + ): + errors.append("next_steps must be a list of non-empty strings when present") + + return ValidationResult(ok=not errors, errors=errors) + + def validate_social_post(entry: dict[str, Any]) -> ValidationResult: errors: list[str] = [] diff --git a/scripts/github/social_candidate.schema.json b/scripts/github/social_candidate.schema.json new file mode 100644 index 0000000..5e2cd88 --- /dev/null +++ b/scripts/github/social_candidate.schema.json @@ -0,0 +1,195 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Decodex social candidate record", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "slug", + "repo", + "channel", + "target_account", + "mode", + "priority", + "audience", + "candidate_text", + "source_refs", + "evidence_notes", + "claims", + "decision" + ], + "properties": { + "schema": { + "const": "social_candidate/v1" + }, + "slug": { + "type": "string", + "minLength": 1 + }, + "repo": { + "type": "string", + "pattern": "^[^/]+/[^/]+$" + }, + "channel": { + "const": "x" + }, + "target_account": { + "const": "decodexspace" + }, + "mode": { + "type": "string", + "enum": [ + "release_pulse", + "release_rollup", + "practical_explainer", + "operator_impact", + "thread", + "watch_note" + ] + }, + "priority": { + "type": "string", + "enum": ["critical", "high", "normal", "low"] + }, + "audience": { + "type": "string", + "minLength": 1 + }, + "candidate_text": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 280 + } + }, + "source_refs": { + "type": "object", + "additionalProperties": false, + "anyOf": [ + { + "required": ["upstream_reviews"] + }, + { + "required": ["upstream_impacts"] + }, + { + "required": ["signals"] + }, + { + "required": ["release_deltas"] + }, + { + "required": ["urls"] + } + ], + "properties": { + "upstream_reviews": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "upstream_impacts": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "signals": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "release_deltas": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "urls": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^https://" + } + } + } + }, + "evidence_notes": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "claims": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["text", "evidence", "confidence"], + "properties": { + "text": { + "type": "string", + "minLength": 1 + }, + "evidence": { + "type": "string", + "minLength": 1 + }, + "confidence": { + "type": "string", + "enum": ["confirmed", "likely", "weak"] + } + } + } + }, + "decision": { + "type": "object", + "additionalProperties": false, + "required": ["worthiness", "reason", "idempotency_key"], + "properties": { + "worthiness": { + "type": "string", + "enum": ["publish", "defer", "skip"] + }, + "reason": { + "type": "string", + "minLength": 1 + }, + "idempotency_key": { + "type": "string", + "minLength": 1 + } + } + }, + "caveats": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "next_steps": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + } +} diff --git a/scripts/github/social_post.schema.json b/scripts/github/social_post.schema.json index e2bf41f..0883e4e 100644 --- a/scripts/github/social_post.schema.json +++ b/scripts/github/social_post.schema.json @@ -285,6 +285,7 @@ } }, "media_refs": { + "description": "X media readback URLs, external media pointers, content hashes, or explicitly operator-approved checked-in sample paths. Generated media files are not committed by default.", "type": "array", "items": { "type": "string", diff --git a/scripts/github/upstream_review.schema.json b/scripts/github/upstream_review.schema.json index 14b178b..5608cde 100644 --- a/scripts/github/upstream_review.schema.json +++ b/scripts/github/upstream_review.schema.json @@ -143,7 +143,7 @@ "properties": { "type": { "type": "string", - "enum": ["none", "upstream_impact", "signal_entry", "social_post", "linear_followup"] + "enum": ["none", "upstream_impact", "signal_entry", "social_candidate", "linear_followup"] }, "reason": { "type": "string",