Skip to content

feat: generic artifact shapes — shape-based save_artifact + backend migration (#796 PR A) - #2313

Merged
lsm merged 6 commits into
devfrom
space/generic-artifact-shapes-shape-based-save-artifact-rendering
Aug 2, 2026
Merged

feat: generic artifact shapes — shape-based save_artifact + backend migration (#796 PR A)#2313
lsm merged 6 commits into
devfrom
space/generic-artifact-shapes-shape-based-save-artifact-rendering

Conversation

@lsm

@lsm lsm commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Replaces the freeform save_artifact type system with a closed, domain-agnostic shape vocabulary (link, commit_set, check, metric, decision, note) plus a freeform kind semantic hint — PR A of #796. A PR, an issue, and a preview page are all link with different kind; infra never names pr/review/etc.

What changed

  • save_artifact({ shape, kind?, key?, summary?, data? }) validates against the closed set, rejects unknown shapes, and derives a shape-aware identity key: note is a single rolling-status upsert (stops per-round bloat), link is one-per-kind, check/metric keyed by name, decision single-terminal or multi-round via explicit key.
  • A data-aware legacy shim keeps in-flight agents working: { type } maps to a shape (progress→note, result→link|decision by content, review→decision, pr→link kind:pr) and bypasses strict validation; unknown legacy types are rejected.
  • Every backend reader/writer migrated off legacy types (review auto-save → decision kind:review round-N; resolvePrUrlForRun x3 → link kind:pr; result-summary readers → decision). Infra-level Runtime Execution Contract prompts updated.
  • Migration 164 backfills existing rows (legacy type→shape, link url normalization, collapses per-(run,node) note rows to latest current) — idempotent.

Deferred to follow-up PRs: UI shape-based rendering (PR B), and relocating coding-specific writers/readers out of daemon core + coding-workflow prompt text (PR C). The UI keeps working today because ArtifactCard already detects by data shape.

Verified: shared/daemon/web suites green; the only daemon failures (3 in space-mcp-handlers.test.ts) are pre-existing (reproduce on the clean base, unrelated to artifacts). Note: check:test-quality has one pre-existing issue in provider-registry.test.ts (also untouched here).

lsm added 3 commits August 2, 2026 13:21
Introduce a domain-agnostic, closed SHAPE vocabulary (link, commit_set,
check, metric, decision, note) with a freeform KIND semantic hint. A PR,
an issue, and a preview page are all 'link' with different kinds; infra
never names domain kinds.

The new artifact-shapes module is pure (no zod/I/O) so daemon and web can
share it, and provides:
- ARTIFACT_SHAPES / ArtifactShape closed set + per-shape data contracts
- isArtifactShape / validateArtifactShape (per-shape required-field checks)
- deriveArtifactKey (shape-aware identity: note→single upsert, link→one
  per kind, check/metric→name, decision→key|kind|'current')
- resolveLegacyShape (data-aware legacy type→shape router; result is
  overloaded so it picks link vs decision by content)
- normalizeLinkData (copy pr_url/review_url onto data.url for link rows)

Task #796 (PR A — backend foundation).
…ackend

Replace the freeform save_artifact type system with the closed shape
vocabulary. save_artifact now takes { shape, kind?, key?, summary?, data? },
validates the payload against the per-shape contract, rejects unknown shapes,
and derives a shape-aware identity key so a 'note' is a single rolling-status
upsert (no per-round growth) and a 'link' is one-per-kind.

A data-aware legacy shim keeps in-flight agents working: { type } is mapped
to a shape (progress→note, result→link|decision by content, review→decision,
pr→link kind:pr) and bypasses strict validation since it predates the
contracts; unknown legacy types are still rejected.

Migrate every backend reader/writer off legacy types:
- review auto-save (review-posted-gate) → decision kind:review, keyed round-N
- list_peers rolling status → note (data.text ?? summary)
- resolvePrUrlForRun (x3) → link kind:pr, data.url, with legacy fallback
- terminal result-summary readers (mark_complete, run completion, Forge gap
  detection) → decision summaries
Update the infra-level Runtime Execution Contract prompts to the shape API.

Migration 164 backfills existing rows: legacy type→shape (data-aware), link
url normalization, and collapses per-(run,node) note rows to the single most
recent 'current' row so storage stops growing per round. Idempotent.

Coding-workflow prompt text and physical relocation of coding-specific
writers/readers out of daemon core are deferred to follow-up PRs (B: UI
shape rendering, C: coding-layer mapping + prompts).

Task #796 (PR A — backend foundation).
…-shapes-shape-based-save-artifact-rendering

# Conflicts:
#	packages/daemon/src/storage/schema/migrations.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0546268414

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/storage/schema/migrations.ts Outdated
Comment thread packages/daemon/src/storage/schema/migrations.ts Outdated
Comment thread packages/daemon/src/lib/space/tools/node-agent-tools.ts Outdated
Comment thread packages/shared/src/artifact-shapes.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/space-runtime.ts Outdated
Comment thread packages/shared/src/artifact-shapes.ts Outdated
Comment thread packages/daemon/src/lib/space/tools/node-agent-tools.ts
Comment thread packages/daemon/src/storage/schema/migrations.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 129c8d3160

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/shared/src/artifact-shapes.ts Outdated
Comment thread packages/shared/src/artifact-shapes.ts Outdated
Comment thread packages/daemon/src/storage/schema/migrations.ts
Twelve review findings, all fixed:

- c1: migration now dedupes legacy rows by (run,node,shape,key) keeping the
  latest BEFORE writing, so two legacy types mapping to the same key (e.g.
  review + URL-less result sharing a key) cannot violate UNIQUE and abort
  startup.
- c2: the note collapse targets only ex-progress rows (the append-log bloat);
  unknown freeform types (merge_blocked, merge_conflict_loop, etc.) are
  preserved as distinct notes keyed by their original type, not collapsed.
- c3: unknown legacy types are accepted as a note tagged _legacyType (with a
  distinct key) so active post-approval prompts that still emit
  merge_conflict_loop/merge_blocked/cleanup_warning keep recording state
  instead of erroring.
- c4: a legacy result with a summary now stays a decision (summary visible to
  completion readers) even when it also carries a pr_url; only URL-only results
  become links.
- c5: resolvePrUrlForRun (4 copies) no longer treats an arbitrary data.url as a
  PR URL -- only a link kind:pr (data.url) or an explicit legacy pr_url/prUrl
  qualifies, so an issue/preview link cannot be injected as PR_URL.
- c6: terminal-result readers exclude kind:review decisions (review-round
  feedback is not a terminal outcome) across space-runtime, task-agent-manager,
  and the evolution gap detector.
- c7: deriveArtifactKey honors an explicit key only for decision; note/link/
  check/metric/commit_set always derive, so a caller cannot smuggle in a key to
  create unlimited notes.
- c8: a shape NAME passed via the legacy type alias is validated strictly (no
  bypass) -- only genuine legacy semantic types skip validation.
- c9: list_artifacts maps legacy type filters to their shape sets (result ->
  decision+link, progress -> note, review -> decision, pr -> link) so migrated
  rows stay visible to in-flight agents.
- c10: metric validation rejects non-scalar values (array/object/boolean).
- c11: note validation accepts a bare timestamp (data.ts).
- c12: a legacy-artifact reconciliation runs outside the one-shot marker
  (mirroring migration 163) so legacy rows written by an older binary after a
  rollback are caught on every startup.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c9c51d4a4b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/lib/space/runtime/space-runtime.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/space-runtime.ts Outdated
Comment thread packages/daemon/src/lib/space/tools/node-agent-tools.ts Outdated
Comment thread packages/daemon/src/lib/space/tools/node-agent-tools.ts Outdated
Comment thread packages/shared/src/artifact-shapes.ts Outdated

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Review by glm-5.1 (NeoKai)

Model: glm-5.1 | Client: NeoKai | Provider: GLM

Recommendation: REQUEST_CHANGES — one in-scope correctness regression in a reader this PR modifies, plus two robustness items I'm concurring with from the other review. The foundation itself is solid.

Verified good

  • All 12 prior findings are genuinely resolved (read each fix in source, not just the replies).
  • Migration 166 is sound: legacy rows are deduped by (run,node,shape,key) with losers deleted before any UPDATE, so two legacy types mapping to the same key can't violate UNIQUE and abort startup. Idempotent; the outside-marker reconcile mirrors migration 163, is a cheap COUNT(*) no-op once clean, and safely catches rollback stragglers. The note collapse is correctly limited to ex-progress rows — unknown freeform types are preserved as distinct notes.
  • Every backend reader is migrated off legacy types; no orphaned artifactType:'progress'|'result'|'review'|'pr' reads remain in daemon/web source (only the intentional list_artifacts shim and web test fixtures). Terminal-result readers correctly exclude kind:'review'. The four resolvePrUrlForRun copies no longer treat an arbitrary data.url as a PR URL.
  • CI: daemon unit/online + lint/typecheck/knip all green. The lone Web Tests failure is ui/Dropdown > should close when Escape key is pressed — a jsdom keyboard flake in a component this PR does not touch; it passes locally (246/246 files). Not a regression.

P2 (blocking) — list_peers reports the wrong note after migration — see inline comment on node-agent-tools.ts. This reader was changed by this PR (progressnote), so it's in scope and a ~2-line fix.

P2 — concur: terminal-result reader is a one-kind denylist (space-runtime.ts:9568, also evolution-episode-service.ts and task-agent-manager.ts)
kind !== 'review' is a denylist. DecisionArtifactData also models gate approvals; once any non-terminal decision with a summary and a different kind is written, it gets persisted as the task's terminal result. Not a regression vs. the old result-with-summary behavior, but the point of the shape split is a positive contract — please add an explicit terminal discriminator (e.g. a kind:'outcome'/terminal flag) before agents freely use decisions in PR C.

P2 — concur: resolvePrUrlForRun ignores recency across forms (space-runtime.ts:4333 + 3 copies)
The first pass returns the first link kind:pr it finds and never reaches the legacy pr_url/prUrl fallback. While the built-in prompts still emit legacy result/pr_url rows, a run with an older link kind:pr and a newer legacy pr_url (e.g. a superseded PR) will route the merge off the stale PR. Suggestion: collect both eligible forms and choose by updatedAt, as the old recency walk did.

P3 (notes)

  1. Review auto-save key round-${count} (node-agent-tools.ts:1101) can collide with a caller-authored round-N; safe today because cycles are sequential, and pre-existing — not introduced here.
  2. deriveArtifactKey('decision', …, explicitKey) ignores kind when an explicit key is given (artifact-shapes.ts:229), so two decision streams sharing an explicit key but differing in kind would collide — no practical collision today (only review uses explicit keys).
  3. Transition: the legacy append:true path still inserts a new row per call for merge_blocked/merge_conflict_loop/result-append, so per-round bloat for those types persists until PR C migrates the prompts. The "storage stops growing per-round" criterion is fully met only after PR C.

Comment thread packages/daemon/src/lib/space/tools/node-agent-tools.ts Outdated
Five follow-up findings from the second review pass:

- c13 (P1): terminal-result readers used a one-kind denylist (kind != 'review').
  Switched to a positive discriminator — only a kind-less decision (the bare
  terminal form; legacy result -> decision carries no kind, review/gate carry a
  kind) counts as terminal. Applied across space-runtime, task-agent-manager,
  and the evolution gap detector.
- c14 (P1): resolvePrUrlForRun (4 copies) prioritized an older link kind:pr over
  a newer legacy pr_url row. Now collects every eligible candidate (link kind:pr
  data.url + legacy pr_url/prUrl) and returns the most recently updated, so a
  superseded PR is never routed into the merge.
- c15 (P2): list_peers read the last note by created_at, but a node can hold
  several notes (rolling 'current' + migrated unknown types); the appended
  merge_blocked/merge_conflict_loop notes won. Now selects the 'current'-keyed
  note, falling back to max(updatedAt).
- c16 (P2): review auto-save used a count-based cycle that could upsert onto an
  existing round-N. Now derives the next round from the max numeric suffix among
  existing review decisions (handles sparse keys and both legacy cycle-N and
  namespaced review:round-N), guaranteeing an unused key.
- c17 (P2): deriveArtifactKey ignored kind under an explicit decision key, so
  kind:review round-0 and kind:gate round-0 collided. Explicit decision keys are
  now namespaced by kind (review:round-0 vs gate:round-0).

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Review by glm-5.1 (NeoKai)

Model: glm-5.1 | Client: NeoKai | Provider: GLM

Recommendation: APPROVE — all second-pass findings addressed and verified; zero open findings.

Verified each fix on ad0fcde30:

  • list_peers note selection (c15): now reads the current-keyed note, falling back to max(updatedAt) — appended merge_blocked/merge_conflict_loop notes no longer shadow the rolling status.
  • terminal-result discriminator (c13): all three readers require a kind-less decision with a summary (legacy resultdecision is kind-less; review/gate are excluded). Confirmed no current path writes a terminal result carrying a kind.
  • resolvePrUrlForRun recency (c14): all four copies collect eligible candidates (link kind:'pr' via data.url, or legacy pr_url/prUrl) and return the max-updatedAt; a generic data.url still never qualifies.
  • review-round keying (c16): next round derived from the max numeric suffix across existing review keys (handles legacy cycle-N and namespaced review:round-N) — no overwrite of sparse/caller-authored rounds.
  • decision key namespacing (c17): deriveArtifactKey namespaces explicit decision keys by kind, so two streams (review:round-0 vs gate:round-0) never collide.

CI on ad0fcde30 is fully green (Web Tests, All Tests Pass, coverage gate, lint/typecheck, all daemon unit/online shards). PR is open, mergeable, and all 18 review threads are resolved. Migration 166 remains sound (dedup-before-write, idempotent, rollback-safe reconcile).

One non-blocking note carried to PR C: the legacy append:true path still grows a row per call for merge_blocked/merge_conflict_loop/result-append until the coding-workflow prompts migrate — expected and out of scope for PR A.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ad0fcde304

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/lib/space/tools/node-agent-tools.ts
Comment thread packages/daemon/src/lib/space/tools/node-agent-tools.ts
Comment thread packages/shared/src/artifact-shapes.ts Outdated

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Review by glm-5.1 (NeoKai)

Model: glm-5.1 | Client: NeoKai | Provider: GLM

Recommendation: REQUEST_CHANGES (against ad0fcde30) — three valid findings from the latest pass. I can see fixes for all three are already in progress in the worktree; this review records them against the committed head. Will re-approve once they land as a new commit + CI is green. Human: please do not merge ad0fcde30 — wait for the follow-up commit.

  1. (P2) resolvePrUrlForRun must require the link shape, not just kind:'pr'. All four copies read data.url for any artifact with data.kind === 'pr'. CheckArtifactData legitimately allows both kind and url, so a check { kind:'pr', url:<CI run URL> } would have its CI URL misread as the run's PR and injected into gate eval / gh pr ops. Fix: gate on a.artifactType === 'link' && a.data.kind === 'pr' (non-link kind:'pr' falls through to the legacy pr_url/prUrl check only). (Already applied in worktree at all 5 sites.)

  2. (P2, most important) The registered save_artifact tool description is stale. tool('save_artifact', …) still advertises "type is fully generic: use 'progress'/'result'/'review' or any custom label" and that append controls identity — contradicting the new closed shape schema. Agents following the tool description (not the schema) will emit type:'test_result' etc., silently stored as note and invisible to shape consumers. This is the agent-facing contract; it should ship in PR A. Fix: advertise the shape set + kind, demote type to a compat alias (and update list_artifacts). (Already applied in worktree.)

  3. (P2) commit_set identity ignores repo. deriveArtifactKey keys on branch only, so two repos sharing a branch (repo-a/main, repo-b/main) collapse. Fix: key on ${repo}:${branch} with the existing fallbacks. (Already applied in worktree.)

The c13–c17 fixes from the prior round remain correct and verified; CI on ad0fcde30 was fully green. These three are the only open items.

… commit_set

Third-pass review findings (all agent-facing/correctness):

- c18 (P1): resolvePrUrlForRun (5 copies) read data.url for any artifact with
  kind:'pr', but CheckArtifactData also allows kind+url, so a check
  {kind:'pr', url:<CI url>} would be misread as the PR URL. Now requires
  artifactType === 'link' && kind === 'pr' before reading data.url; non-links
  fall through to legacy pr_url/prUrl fields only.
- c19 (P2): the registered tool('save_artifact') description still advertised
  freeform type/append, contradicting the shape schema — agents following it
  wrote type:'test_result' which silently became a note. Rewrote the
  save_artifact + list_artifacts tool descriptions to advertise shapes/kind and
  demote type to a compat alias.
- c20 (P2): deriveArtifactKey for commit_set used branch only, so repo-a/main
  and repo-b/main collided. Identity is now repo:branch (falling back to either,
  then 'default').

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Review by glm-5.1 (NeoKai)

Model: glm-5.1 | Client: NeoKai | Provider: GLM

Recommendation: APPROVE — final re-approval against 1675ab7b5.

Round-4 verification (fresh pass)

All three round-3 findings are addressed in committed 1675ab7b5 and re-verified independently:

  • c18 — PR-kind URLs restricted to the link shape. resolvePrUrlForRun / dispatchPostApproval PR resolver now require artifactType === 'link' && data.kind === 'pr' before a non-link artifact's stray data.url is treated as a PR URL. Confirmed at all 5 resolver sites (node-agent-tools.ts, space-runtime.ts ×2, task-agent-manager.ts, space-runtime-service.ts). Closes the latent leak where a check/decision carrying both kind and url could masquerade as a PR link.
  • c19 — Tool descriptions advertise shapes. save_artifact now documents the closed shape vocabulary (link/commit_set/check/metric/decision/note) + freeform kind, with type demoted to a compat alias. list_artifacts description updated to match. This is the agent-facing surface, so it matters for correct caller behavior going forward.
  • c20 — commit_set identity is repo:branch. deriveArtifactKey('commit_set') returns ${repo}:${branch} when both are present, so repo-a/main and repo-b/main no longer collapse onto one row. Falls back to branch || repo || 'default'.

Verification evidence

  • Local tests: migration-166 backfill 8/8 ✓; node-agent-tools (save/list/list_peers) 171/171 ✓.
  • CI on 1675ab7b5: Web Tests ✓, Lint/Knip/Format/Type ✓, all Daemon Unit + Online shards ✓. Only Coverage Quality Gate still in_progress (informational — Finalize Coveralls already succeeded; not a test gate). mergeStateStatus: CLEAN.
  • Review threads: 21/21 resolved, 0 unresolved.
  • Shape invariant holds: infra vocabulary stays closed; pr/review never named in infra — only as kind on link/decision. Legacy shim + backfill migration remain idempotent and UNIQUE-safe.

Zero P0–P3 findings. Ready for the human to squash-merge into dev.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1675ab7b54

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/storage/schema/migrations.ts
Comment thread packages/daemon/src/storage/schema/migrations.ts
Comment thread packages/daemon/src/lib/space/tools/node-agent-tools.ts
Comment thread packages/shared/src/artifact-shapes.ts
Comment thread packages/daemon/src/lib/space/tools/node-agent-tools.ts

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Review by glm-5.1 (NeoKai)

Model: glm-5.1 | Client: NeoKai | Provider: GLM

Post-approval triage of 5 new inline findings (chatgpt-codex bot) on 1675ab7b5. I validated each independently against the committed code and the pre-shape history on origin/dev. Recommendation: APPROVE for merge stands — the single P1 is invalid; the two real P2s are non-correctness quality/fidelity gaps routed to Coding for a tiny follow-up.

P1 — "review/result key collision" → INVALID (debunked)

The bot's premise — "a node has a legacy review and terminal result with the same non-empty key" — cannot occur. On origin/dev, review rows were auto-keyed cycle-${cycle} (node-agent-tools.ts:1091) and terminal result rows were always written append:true → random ${Date.now()}-${rand} keys (node-agent-tools.ts:23, :1511). The two schemes never overlap, so the migration's originalKey || deriveArtifactKey (migrations.ts:11195) cannot collapse a review onto a result. No data loss. The bot's code-reading (that the migration preserves originalKey) is accurate; the claimed impact is not.

P2 — extractArtifactDetail misses new-shape fields → REAL (non-blocking)

evolution-scope-service.ts:936-949 reads only summary / pr_url / review_url / status / ..., not url / text / recommendation. Migrated rows survive (normalizeLinkData keeps pr_url alongside the new url), but a fresh { shape:'link', data:{ url, kind } } yields "no artifact detail" in persisted evolution evidence. Fix: add url, text, recommendation to the field list (~1 line). Routed to Coding.

P2 — list_artifacts legacy filter over-returns by kind → REAL (non-blocking)

node-agent-tools.ts:1703-1706 maps type:'review'['decision'], type:'pr'['link'] without post-filtering on data.kind, so { type:'pr' } now also returns issue/preview/doc links (and { type:'review' } returns the terminal result decision). Compat-shim fidelity drift from the old exact-type semantics. Fix: post-filter the mapped-shape results by the legacy semantic kind. Routed to Coding.

P3 — two unknown legacy types sharing a key collide (migrations.ts:11173) → theoretical

Requires two different unknown types with the same non-empty key. The old code defaulted unknown-type keys to '' → distinct-by-type (originalKey || originalType), so collision needs a caller that passed identical explicit keys across two unknown types. Not observed.

P3 — unknown-legacy write ignores keyArg (node-agent-tools.ts:1648) → theoretical

Only affects an in-flight caller using one unknown type with multiple explicit keys (e.g. audit/phase-1, audit/phase-2); the active post-approval prompts (merge_blocked, cleanup_warning) write one rolling status per type, so this is keyed-by-type intentionally.

Net

The headline P1 is debunked (no data loss). The two real P2s are reader-migration completeness items with no correctness or data-loss impact; Coding has been notified for a small follow-up (round-5 or PR C). The foundation — closed shape vocabulary, per-shape validation, UNIQUE-safe dedup-before-write migration, and the migrated readers — remains sound and CI-green.

@lsm
lsm merged commit 49d3719 into dev Aug 2, 2026
36 checks passed
@lsm
lsm deleted the space/generic-artifact-shapes-shape-based-save-artifact-rendering branch August 2, 2026 19:16
lsm added a commit that referenced this pull request Aug 2, 2026
Dev advanced by one commit (49d3719, PR #2313 generic artifact shapes)
which introduced its own migration 166 — colliding with the space_tasks
migration I had just renumbered to 166. Resolution:

- migrations.ts: renumber the space_tasks rate/usage-limited migration
  166 -> 167 (table space_tasks_m166_new -> space_tasks_m167_new, marker
  migration_167). Dev's M166 (artifact-shape backfill + legacy reconcile)
  kept intact and registered before 167.

- space-runtime.ts / space-runtime-service.ts / task-agent-manager.ts /
  space.ts auto-merged cleanly (artifact-shape additions don't overlap the
  rate-limit paths).

Verified: lint/format/knip/typecheck/db-schema-parity/space-task-handler-tests
clean; daemon shards 4-space-migrations-a/b, 4-space-storage,
5-space-runtime-a/b, 5-space-agent-other all pass.
lsm added a commit that referenced this pull request Aug 2, 2026
Replace the detectRenderer(data) data-shape sniffing (and the hardcoded
GITHUB_PR_RE GitHub-PR special-case) with a dispatch on artifact.artifactType,
which after the generic-shapes migration holds a value from the closed
ArtifactShape vocabulary.

Per-shape renderers: link (icon/label by data.kind — kind:'pr' is a PR row,
kind:'issue' an issue row, etc.), commit_set (commit list + +/- totals),
check (status chip + counts), metric (name value unit -> target), decision
(recommendation badge + summary/counts), note (status text line). A default
renderer handles any shape, known or not.

Tests now seed shape-typed artifacts instead of relying on data-shape
detection. TaskArtifactsPanel is unchanged — it already renders ArtifactCard.

Depends on the closed shape vocabulary from #2313 (PR A), included here as
commit 4e26d69 since PR A is unmerged.
lsm added a commit that referenced this pull request Aug 3, 2026
Replace the detectRenderer(data) data-shape sniffing (and the hardcoded
GITHUB_PR_RE GitHub-PR special-case) with a dispatch on artifact.artifactType,
which after the generic-shapes migration holds a value from the closed
ArtifactShape vocabulary.

Per-shape renderers: link (icon/label by data.kind — kind:'pr' is a PR row,
kind:'issue' an issue row, etc.), commit_set (commit list + +/- totals),
check (status chip + counts), metric (name value unit -> target), decision
(recommendation badge + summary/counts), note (status text line). A default
renderer handles any shape, known or not.

Tests now seed shape-typed artifacts instead of relying on data-shape
detection. TaskArtifactsPanel is unchanged — it already renders ArtifactCard.

Depends on the closed shape vocabulary from #2313 (PR A), included here as
commit 4e26d69 since PR A is unmerged.
lsm added a commit that referenced this pull request Aug 3, 2026
…2314)

* feat(web): render ArtifactCard by shape, with kind as icon/label

Replace the detectRenderer(data) data-shape sniffing (and the hardcoded
GITHUB_PR_RE GitHub-PR special-case) with a dispatch on artifact.artifactType,
which after the generic-shapes migration holds a value from the closed
ArtifactShape vocabulary.

Per-shape renderers: link (icon/label by data.kind — kind:'pr' is a PR row,
kind:'issue' an issue row, etc.), commit_set (commit list + +/- totals),
check (status chip + counts), metric (name value unit -> target), decision
(recommendation badge + summary/counts), note (status text line). A default
renderer handles any shape, known or not.

Tests now seed shape-typed artifacts instead of relying on data-shape
detection. TaskArtifactsPanel is unchanged — it already renders ArtifactCard.

Depends on the closed shape vocabulary from #2313 (PR A), included here as
commit 4e26d69 since PR A is unmerged.

* fix(web): address review — safe-URL links, commit_set guard, minimal-PR id, review evidence

Addresses the four review findings on #2314 (security P1 + three P2s):

1. Safe URL schemes (P1, security): LinkCard/CheckCard/DecisionCard now bind
   href only for http(s) URLs via safeHref(); agent-controlled javascript:/data:/
   custom-scheme URLs render as plain text. Defense-in-depth: validateArtifactShape
   ('link') also rejects non-http(s) URLs at save time.
2. commit_set crash guard (P2): filter null/non-object commit entries before
   dereferencing, so {commits:[null]} no longer throws and blanks the panel.
3. Minimal PR identifier (P2): a {url, kind:'pr'} link with no number/title now
   falls back to the URL as its label instead of a bare 'Pull Request'.
4. Legacy review evidence (P2): DecisionCard surfaces data.url/data.review_url as
   a 'review' link so review-history rows (mapped review->decision, carrying
   review_url not recommendation) are not rendered blank.

Tests cover all four behaviors plus the new link scheme validation.

* fix(web): normalize legacy artifact types at the UI boundary

Until the backend producer + DB migration (PR A commit 2) land, daemon rows
still carry pre-shape types (progress/result/pr/review) with no backfill, so the
shape-based dispatch rendered every real artifact as GenericCard — a regression
vs. the old data-shape detection.

ArtifactCard now resolves the effective shape at the render boundary:
isArtifactShape(artifactType) for post-migration rows, falling back to
resolveLegacyShape (progress->note, result->link|decision, pr->link,
review->decision) for legacy rows. Link rows also get normalizeLinkData so a
pr_url/review_url is copied onto data.url. Truly unknown types still fall to the
default renderer.

Idempotent: once the backend stores shapes, the legacy branch is never taken.
Covered by a new 'legacy type normalization' test block (6 cases).

* fix(web): preserve mixed-content results + reject non-scalar metric values

Two more automated review findings:

1. Mixed-content legacy results (P2): the full-stack QA workflow writes a result
   artifact carrying pr_url + summary + test_output + browser-validation evidence.
   resolveLegacyShape('result') was mapping any URL-bearing result to a pure
   link, so LinkCard hid the QA summary/output. Refined so a result with a URL
   AND content maps to a decision (summary renders); URL-only results stay
   links. DecisionCard also surfaces pr_url (label 'view') so the PR link survives.

2. Non-scalar metric values (P2): validateArtifactShape('metric') accepted
   {value:{...}} (only checked non-null) and MetricCard stringified it to
   '[object Object]'. Validator now requires number|string; MetricCard only
   renders scalar values so unvalidated/legacy data can't show [object Object].

Tests: shared (result-with-content->decision, metric rejects non-scalar) +
web (mixed QA result preserves summary + view link; metric object value not
stringified).

* fix(shared): recognize merged_pr_url (and any *_url field) for legacy link routing

Round-7 review finding (P2): the post-approval merge audit writes a legacy
result { merged_pr_url, merged_at, approval_source }, but resolveLegacyShape's
url check only knew url/pr_url/prUrl/review_url, so the row routed to decision
and rendered a bare badge — dropping the merged PR URL. migrations.ts already
treats merged_pr_url as a URL field, so this was an inconsistency.

Rather than re-enumerate field names (the codebase has several *_url variants in
artifact data: pr_url, prUrl, review_url, merged_pr_url, image_url, ...), both
resolveLegacyShape('result') and normalizeLinkData now use a shared findLinkUrl
helper that matches any key that is 'url' or ends in '_url'/'Url'. Covers the
migrations.ts list and stops the per-variant whack-a-mole. Existing url-only
tests still pass. Secondary audit metadata (merged_at/approval_source) and the
full producer split stay with PR C.

Tests: shared result+merged_pr_url→link and normalize merged_pr_url; web legacy
merge-audit result renders the merged PR link, not an empty card.

* fix(web): DecisionCard — validate each evidence URL independently

Greptile round-11 findings (both real, edge-case):

1. Label/href mismatch: when a decision carried both data.url and review_url,
   href resolved to data.url (won the ||) but the label read 'review'.
2. Non-http data.url dropped the evidence link: safeHref(url || reviewUrl || pr_url)
   validated only the first non-empty candidate, so a non-http data.url returned
   null and the valid review_url/pr_url fallbacks were never tried.

Fix: compute safeHref per candidate and pick the first http(s) one, tracking which
won so the link label matches the selected URL. Two new tests cover both cases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant