Commit Graph
517 Commits
Author SHA1 Message Date
Cursor AgentandTom Huang e026a54fd9 docs(daemon): record V-series landing in living plan + CHANGELOG
Plan V2 — sync docs/plans/plugins-implementation.md and CHANGELOG.md
with the V-series shipped this turn:

  V1. OD_BUNDLED_ATOM_PROMPTS default flipped to ON

Living plan now reads:
  Phase 4 \u2192 'full incl. OD_BUNDLED_ATOM_PROMPTS default ON'

Remaining backlog: postgres adapter wiring inside the DaemonDb
resolver — the only stubbed item left.

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 16:16:29 +00:00
Cursor AgentandTom Huang b7f0fc0d96 feat(daemon): flip OD_BUNDLED_ATOM_PROMPTS default to ON (Phase 4)
Plan V1 / spec §23.4.

The bundled SKILL.md fragment library now covers every Phase 6/7/8
atom impl shipped this changeset (build-test, code-import,
design-extract, figma-extract, token-map, rewrite-plan, patch-edit,
diff-review, handoff). The §3.M3-M5 audit-block on flipping the
default-active flag is therefore lifted.

The new policy:

  default:                      ON   (activeStageBlocks render)
  OD_BUNDLED_ATOM_PROMPTS unset ON
  OD_BUNDLED_ATOM_PROMPTS=''    ON
  OD_BUNDLED_ATOM_PROMPTS=1     ON
  OD_BUNDLED_ATOM_PROMPTS=0     OFF  (explicit opt-out for
                                      snapshot replay against
                                      pre-§3.V1 daemons + regression
                                      bisects that need byte-equal
                                      pre-flip prompts)
  OD_BUNDLED_ATOM_PROMPTS=*     ON   (forward-compat)

Effect: a run with a plugin snapshot whose pipeline carries any
of the bundled atoms now ALWAYS gets the matching SKILL.md fragments
spliced in as 'Active stage' blocks ahead of the rest of the prompt,
without operators having to set the env var. Runs without a
snapshot keep byte-equal prompts (the snapshotId guard short-
circuits the build).

Daemon tests: 1661 \u2192 1666 (+5 cases on
plugins-bundled-atom-prompts-default: ON when unset / empty / '1'
/ unknown values; OFF only when explicit '0').

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 16:15:31 +00:00
Cursor AgentandTom Huang 0ddd29aeb6 docs(daemon): record U-series landing in living plan + CHANGELOG
Plan U2 — sync docs/plans/plugins-implementation.md and CHANGELOG.md
with the U-series shipped this turn:

  U1. S3ProjectStorage live impl via AWS SigV4

Living plan now reads:
  Phase 5 \u2192 'full incl. live S3 impl; postgres adapter still stubbed'

Remaining backlog:
  - Promote OD_BUNDLED_ATOM_PROMPTS=1 to default (audit-blocked)
  - Postgres adapter wiring inside DaemonDb resolver

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 16:11:10 +00:00
Cursor AgentandTom Huang 9b2ce2bbc4 feat(daemon): S3ProjectStorage live impl via AWS SigV4 (Phase 5)
Plan U1 / spec §15.6.

apps/daemon/src/storage/aws-sigv4.ts ships a minimal AWS Signature
V4 signer using only node:crypto. No `@aws-sdk/*` dep is pulled
in; the OD daemon ships without an extra ~60 MB of SDK code on
disk.

  signSigV4({ method, path, query, headers, body, region, service,
              credentials, now? }) \u2192 { authorization, amzDate,
                                          contentSha256 }

The signer mutates the supplied headers map by adding the four
required amz fields ('x-amz-date', 'x-amz-content-sha256',
optional 'x-amz-security-token', and the final 'authorization').
The 'now' override pins signatures for tests.

apps/daemon/src/storage/project-storage.ts S3ProjectStorage now
implements the five operations the ProjectStorage interface
declares — all five round-trip through pluggable fetch:

  readFile    \u2192 GET    /<key>
  writeFile   \u2192 PUT    /<key>          (with x-amz-content-sha256)
  deleteFile  \u2192 DELETE /<key>          (idempotent on 404)
  statFile    \u2192 HEAD   /<key>          (returns null on 404)
  listFiles   \u2192 GET    /?list-type=2&prefix=<projectId>/
                walks NextContinuationToken for paginated buckets.

Style:
  - Default to virtual-host-style endpoints (`<bucket>.s3.<region>.amazonaws.com`).
  - When OD_S3_ENDPOINT is set, switch to path-style (`<endpoint>/<bucket>/<key>`) for
    Aliyun OSS / Tencent COS / Huawei OBS / MinIO.

Credentials are read in this order:
  OD_S3_ACCESS_KEY_ID / OD_S3_SECRET_ACCESS_KEY / OD_S3_SESSION_TOKEN
  AWS_ACCESS_KEY_ID  / AWS_SECRET_ACCESS_KEY  / AWS_SESSION_TOKEN
\u2026so existing AWS toolchain setups (`aws configure` exporters,
IAM-role pods) drop in without renaming.

Daemon tests: 1650 \u2192 1661 (+11).

  +4 plugins-aws-sigv4 cases:
    - AWS-published reference vector (GetObject) hashes byte-equal.
    - List-bucket query stays canonical-sorted.
    - x-amz-security-token forwards through SignedHeaders.
    - encodeS3PathSegment handles unreserved / reserved chars.

  +7 storage S3 cases (replacing the prior throw-only stub):
    - PUT signs + reports ProjectFileMeta back.
    - GET returns body bytes; 404 \u2192 NOT_FOUND.
    - HEAD returns null on 404 + parses Content-Length / Last-Modified.
    - DELETE swallows 404, rejects 500.
    - LIST parses ListBucketV2 XML + walks NextContinuationToken.
    - endpoint override switches to path-style.
    - resolveProjectStorage falls back to AWS_* env vars when OD_*
      knobs are unset.

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 16:10:05 +00:00
Cursor AgentandTom Huang 798ffaec48 docs(plugins): record T-series landing in living plan + CHANGELOG
Plan T3 — sync docs/plans/plugins-implementation.md and CHANGELOG.md
with the T-series shipped this turn:

  T1. runAndPersistHandoff() + auto-handoff from diff-review bridge
  T2. figma-migration pipeline e2e smoke test

Phase 8 promotion ladder now closes end-to-end without an agent turn:
GenUI Accept \u2192 review/decision.json + handoff/manifest.json with
handoffKind set.

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 16:03:37 +00:00
Cursor AgentandTom Huang 023a9eb56f test(plugins): figma-migration pipeline e2e smoke (Phase 6 entry slice)
Plan T2 / spec §1 / §10 / §21.3.1.

apps/daemon/tests/plugins-figma-migration-e2e.test.ts walks every
Phase 6 atom impl in sequence on a stubbed Figma REST response:

  figma-extract \u2192 figma/{tree.json, tokens.json, meta.json}
                    via injected fetch stub (no network).
  token-map     \u2192 token-map/colors.json crosswalking the lifted
                    SOLID fills onto the active design system.
  diff-review   \u2192 review/decision.json with explicit decision.
  handoff       \u2192 handoff/manifest.json via runAndPersistHandoff()
                    with the right handoffKind from the promotion
                    ladder.

Three cases:

  1. accept happy path \u2192 implementation-plan rung; figma export
     target lands on manifest.exportTargets[].
  2. reject path \u2192 design-only rung even after figma-extract +
     token-map ran successfully.
  3. Bundled scenario folder roster lock — confirms
     plugins/_official/scenarios/od-figma-migration/open-design.json
     ships the canonical stage list (extract \u2192 tokens \u2192 generate
     \u2192 critique) so the §3.O1 fallback resolver always has the
     same target shape to copy.

Daemon tests: 1647 \u2192 1650 (+3 cases on plugins-figma-migration-e2e:
accept, reject, scenario roster).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 16:02:40 +00:00
Cursor AgentandTom Huang 97c3b77f51 feat(plugins): runAndPersistHandoff() + auto-handoff from diff-review bridge
Plan T1 / spec §11.5.1 / §21.5.

apps/daemon/src/plugins/atoms/handoff.ts gains the on-disk shell
runAndPersistHandoff({ cwd, manifestSeed?, exportTarget?, ... })
that round-trips `<cwd>/handoff/manifest.json`:

  1. Read existing manifest (if any) so promotions stay monotonic.
     Fall back to manifestSeed; final fallback is a minimal default.
  2. Call runHandoffAtom() with the chosen seed + caller's targets.
  3. Write the updated manifest back ONLY when something changed.

persistMode in the result distinguishes 'created' / 'updated' /
'skipped' so callers can decide whether to broadcast an event.

The diff-review GenUI bridge (R1) now auto-invokes this helper
after recording the user's decision. The /api/runs/:id/genui/...
respond endpoint therefore writes BOTH:

  <cwd>/review/decision.json     (R1)
  <cwd>/handoff/manifest.json    (T1 — auto-promotes from the new
                                  decision + any prior build-test
                                  signals)

The handoff bridge is best-effort: a failure surfaces on the
ApplyDiffReviewDecisionResult as `handoffError` without failing
the diff-review write.

Phase 8 promotion ladder now closes end-to-end without an agent
turn:

  user clicks 'Accept all' in the web composer's diff-review
   surface
   \u2192 POST /api/runs/:id/genui/__auto_diff_review_<stage>/respond
   \u2192 daemon writes review/decision.json
   \u2192 daemon writes handoff/manifest.json with handoffKind set
     according to the reject/accept + build-test ladder
   \u2192 follow-up od plugin export reads handoff/manifest.json and
     ships the artefact with the right provenance fields.

Daemon tests: 1641 \u2192 1647 (+6 cases on plugins-handoff-persist:
created from seed, round-trips existing manifest with monotonic
promotion, no-op skip on second run, deployable-app rung with
docker export, diff-review bridge auto-creates handoff/manifest.json
on accept, reject auto-stamps design-only, build-test signals
forward through the bridge into 'patch' tier).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 16:00:50 +00:00
Cursor AgentandTom Huang 4c3423a500 docs(plugins): record S-series landing in living plan + CHANGELOG
Plan S3 — sync docs/plans/plugins-implementation.md and CHANGELOG.md
with the S-series shipped this turn:

  S1. runHandoffAtom() pipeline-driven promotion-ladder bridge
  S2. Full code-migration pipeline e2e smoke test

Living plan now records:
  Phase 7 \u2705 + 'full pipeline e2e covered by smoke test'
  Phase 8 \u2705 + 'handoff promotion ladder bridge landed'

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 15:56:54 +00:00
Cursor AgentandTom Huang 999c6cd68a test(plugins): full code-migration pipeline e2e smoke test (Phase 7-8)
Plan S2 / spec §1 / §10 / §20.3 / §21.3.2.

apps/daemon/tests/plugins-code-migration-e2e.test.ts exercises
every Phase 6/7/8 atom impl in sequence on a Next.js fixture repo,
proving the contract chain works end-to-end without any agent /
LLM in the loop:

  code-import     \u2192 code/index.json
  design-extract  \u2192 code/tokens.json (hex colour lifted)
  token-map       \u2192 token-map/colors.json (mapped to design system)
  rewrite-plan    \u2192 plan/{plan.md, ownership.json, steps.json,
                       meta.json}
  patch-edit      \u2192 components/Button.tsx mutated via unified diff;
                       plan/receipts/step-rewrite-button.json written
  build-test      \u2192 critique/build-test.json (no-op commands so we
                       don't shell out to a real toolchain in CI)
  diff-review     \u2192 review/{diff.patch, summary.md, decision.json,
                       meta.json}
  handoff         \u2192 ArtifactManifest.handoffKind='deployable-app'
                       (accept + both signals + cli exportTarget)

Also adds a second case covering the reject ladder rung
(reject + missing build-test \u2192 design-only).

This is the first integration test that walks every code-migration
atom in one go; it locks the inter-atom file contract so a future
PR can't break the chain by, say, renaming code/tokens.json or
adding a required field to plan/steps.json without updating every
downstream reader.

Daemon tests: 1639 \u2192 1641 (+2 cases on
plugins-code-migration-e2e: full chain ending on
deployable-app, reject \u2192 design-only).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 15:55:57 +00:00
Cursor AgentandTom Huang 6fa42ca159 feat(plugins): runHandoffAtom() pipeline-driven bridge (Phase 8 entry slice)
Plan S1 / spec §11.5.1 / §21.5.

apps/daemon/src/plugins/atoms/handoff.ts gains the on-disk bridge
runHandoffAtom({ cwd, manifest, exportTarget?, deployTarget? })
that reads the canonical state previous atoms wrote and returns
the updated manifest with the right handoffKind / exportTargets[]
attached.

Promotion ladder (spec §11.5.1):

  decision='reject'                                       → 'design-only'
  decision='accept'/'partial' AND no build-test report    → 'implementation-plan'
  + build.passing OR tests.passing                        → 'patch'
  + build.passing AND tests.passing AND docker/cli export → 'deployable-app'

Inputs read from cwd:

  <cwd>/review/decision.json       — diff-review atom output
  <cwd>/critique/build-test.json   — build-test atom signals

Both files are optional; missing → the bridge skips the matching
ladder rung. Monotonicity enforced via recordHandoff() — a manifest
that already carries handoffKind='patch' won't demote to
'design-only' even if a follow-up reject arrives (the reject path
documents the rollback escape hatch via enforceMonotonicHandoff:
false).

Returns:

  { manifest, changed, signals: {
      decision?, buildPassing?, testsPassing?, deployable
  } }

Caller (the pipeline runner / od plugin export / docker tools-pack
hook) is responsible for persisting the updated manifest. Calls
are idempotent: re-running across already-recorded targets is a
no-op via the recordHandoff() append-only contract.

Daemon tests: 1629 → 1639 (+10 cases on plugins-handoff-pipeline:
all four ladder rungs (reject → design-only, accept-noBT →
implementation-plan, accept+oneSignal → patch, accept+bothSignals
+docker/cli → deployable-app), partial decision behaves like
accept, missing decision file leaves manifest alone, append-only
exportTargets[] dedupe across re-runs, refuses to demote across
the monotonic invariant, signals forwarding).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 15:53:30 +00:00
Cursor AgentandTom Huang c1f6d1f02d docs(plugins): record R-series landing in living plan + CHANGELOG
Plan R2 — sync docs/plans/plugins-implementation.md and CHANGELOG.md
with the R-series shipped this turn:

  R1. diff-review GenUI \u2192 review/decision.json bridge

Living plan now reads:
  Phase 8 → \u2705 COMPLETE end-to-end. Patch substrate +
    diff-review atom + auto-surface + native UI mount + GenUI \u2192
    runDiffReview bridge all landed.
  Phase 6 → \u2705 COMPLETE.
  Phase 7 → \u2705 COMPLETE.

Remaining backlog:
  - Promote OD_BUNDLED_ATOM_PROMPTS=1 to default (audit-blocked)
  - AWS SDK wiring inside S3ProjectStorage
  - Postgres adapter wiring inside DaemonDb resolver

Changelog names every shipped surface so a reviewer can audit the
delta without reading the plan.

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 15:49:50 +00:00
Cursor AgentandTom Huang a1bfac34f6 feat(plugins): wire diff-review GenUI response \u2192 review/decision.json (Phase 8)
Plan R1 / spec §10.3 / §21.5.

When the user (or agent) responds to the auto-derived
`__auto_diff_review_<stageId>` choice surface, the daemon now
immediately persists the decision into the run's project cwd as
`<cwd>/review/decision.json` so the next pipeline stage (handoff,
typically) sees the user's choice without an extra agent turn.

apps/daemon/src/plugins/atoms/diff-review-genui-bridge.ts

  - isDiffReviewSurfaceId(id)            — owns the
    '__auto_diff_review_' prefix; future renames flow from one
    constant.
  - parseDiffReviewGenuiResponse(value)  — strict JSON validator
    coercing the surface payload into runDiffReview's
    { decision, accepted_files?, rejected_files?, reason? }
    shape. Rejects non-object payloads + unknown decisions.
  - applyDiffReviewDecisionToCwd({ cwd, value, reviewer })
    — end-to-end glue: parses, calls runDiffReview(), returns
    structured ok/error so the caller can decide what to surface.

apps/daemon/src/server.ts POST /api/runs/:runId/genui/:surfaceId/respond
now becomes async and, when isDiffReviewSurfaceId() matches, looks
up the run's projectId via design.runs.get(), resolves the project
cwd via resolveProjectDir(), and calls
applyDiffReviewDecisionToCwd() with reviewer='user' (or 'agent' /
'auto' when respondedBy says so).

Best-effort wiring:
  - Failures are caught and surfaced on the response payload as
    `diffReviewBridge: { ok: false, error }` so the agent can
    retry without the GenUI respond contract regressing.
  - No-project runs return diffReviewBridge: { ok: false, error:
    'run is not linked to a project' }.

Daemon tests: 1618 → 1629 (+11 cases on
plugins-diff-review-genui-bridge: prefix detection, payload
parsing happy + sad paths (non-object, malformed decision,
non-string file lists, optional reason forwarding), end-to-end
applyDiffReviewDecisionToCwd writes review/decision.json on
accept, agent reviewer + reason forwarding, partial-missing-file
rejection, malformed value does NOT touch disk).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 15:48:21 +00:00
Cursor AgentandTom Huang 72ec5fb176 docs(plugins): record Q-series landing in living plan + CHANGELOG
Plan Q3 — sync docs/plans/plugins-implementation.md and CHANGELOG.md
with the Q-series shipped this turn:

  Q1. Native diff-review UI on GenUISurfaceRenderer  (Phase 8 entry slice)
  Q2. figma-extract asset rasterisation second pass  (Phase 6 entry slice)

Living plan now reads:
  Phase 6 → COMPLETE: figma-extract REST + token-map crosswalk +
    asset rasterisation second pass via GET /v1/images.
  Phase 8 → patch + review + auto-surface + native review UI all
    landed. The remaining slice is wiring the GenUI response back
    through runDiffReview() to auto-update review/decision.json
    (today the daemon stores the response on the run; the
    diff-review runner reads it on next invocation).

Changelog names every shipped surface so a reviewer can audit the
delta without reading the plan.

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 15:41:46 +00:00
Cursor AgentandTom Huang d408d98a4c feat(plugins): figma-extract asset rasterisation second pass (Phase 6 entry slice)
Plan Q2 / spec §10.3.1 / §21.3.1.

apps/daemon/src/plugins/atoms/figma-extract.ts now honours
`offlineAssets: false` end-to-end. After the GET /v1/files pass,
the runner picks visible non-text leaf nodes that have a bounding
box, calls Figma's `GET /v1/images/<key>?ids=...&format=svg`
in 50-id chunks, and downloads each returned URL into
`<cwd>/figma/assets/<safe-id>.<ext>`.

Knobs:

  - `assetFormat`:    'svg' (default) | 'png' | 'jpg' | 'pdf'
  - `assetMaxBytes`:  per-asset cap, default 5 MiB
  - `offlineAssets`:  default true (preserves prior behaviour)

Per-id failures DO NOT abort the run — every issue lands in
`meta.unsupportedNodes` with reason='asset-too-large' / 'no
download URL returned' / 'download <status> <statusText>' / 'image
fetch error: <msg>' / 'figma error: <msg>'. The atom continues
through the remaining ids so a single CDN hiccup doesn't lose the
whole batch.

Filename safety: ids like '3:2' are passed through unchanged
(SQLite + JSON friendly); only chars outside [A-Za-z0-9_:-] are
collapsed to '-'.

atomDigest is re-computed after the asset pass settles so the
digest reflects which assets succeeded, even though the raw
bytes themselves are not part of the digest input (we hash the
issues list shape, not blobs).

Daemon tests: 1614 → 1618 (+4 cases on plugins-figma-extract:
multi-id download success, per-id null-URL surfaces issue without
aborting, assetMaxBytes skip + reason capture, offlineAssets=true
default keeps assets/ empty).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 15:40:13 +00:00
Cursor AgentandTom Huang 85beeb58c9 feat(web): native diff-review UI on GenUISurfaceRenderer (Phase 8 entry slice)
Plan Q1 / spec §21.5.

apps/web/src/components/GenUISurfaceRenderer.tsx now ships first-
class branches for the auto-derived diff-review choice surface and
generic single-enum-property choice surfaces.

Diff-review (DiffReviewChoiceSurface):
  - Three top-level buttons: 'Accept all' / 'Reject all' / 'Partial…'.
  - Optional 'Skip' when the host supplies onSkip.
  - Optional notes textarea — forwarded as decision.reason when non-empty.
  - On 'Accept all': submits { decision: 'accept', accepted_files,
    rejected_files: [] } using the touched file list from
    pending.context.touchedFiles. Daemon side default-fills when the
    list is empty.
  - On 'Reject all': symmetric.
  - On 'Partial…': reveals a per-file accept/reject toggle for each
    touched file. Submit refuses locally when ANY file is left
    undecided (mirrors the daemon's 'partial must cover every
    touched file' contract from §3.O5 so the user doesn't ping the
    server with an obviously-invalid payload).
  - Disabled when context.touchedFiles is empty (the daemon's
    default-fill path doesn't help with a partial decision).

Generic choice (GenericChoiceSurface):
  - Detects schemas of shape `{ properties: { <key>: { enum: [...] } } }`
    and renders one button per enum value. Property literally named
    'decision' wins over other enum properties when several are
    declared (so plugin-author-customised diff-review schemas keep
    rendering as accept/reject/partial buttons even if they add
    extra fields).

PendingSurface gains an optional `context: { touchedFiles?: [] }`
field. Future runtime-context entries plug in here without bloating
the GenUISurfaceSpec contract.

Web tests: 586 → 593 (+7 cases on
GenUISurfaceRenderer.diff-review: accept-all default-fill, reject-
all default-fill, partial union, partial blocks on undecided file,
partial disabled when context absent, optional reason forwarding,
generic single-enum choice button group).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 15:36:38 +00:00
Cursor AgentandTom Huang e135739f99 docs(plugins): record P-series landing in living plan + CHANGELOG
Plan P4 — sync docs/plans/plugins-implementation.md and CHANGELOG.md
with the P-series shipped this turn:

  P1. token-map atom impl                       (Phase 6/7 entry slice)
  P2. figma-extract atom impl                   (Phase 6 entry slice)
  P3. Auto-derived choice surface for diff-review (Phase 8 entry slice)

Living plan now reads:
  Phase 6 → COMPLETE: figma-extract REST shell-out + token-map
    crosswalk both implemented. Bundled scenario plugin
    od-figma-migration ships the canonical pipeline. Asset
    rasterisation second pass (offlineAssets=false) stays
    scheduled.
  Phase 7 → COMPLETE (continued from O-series): all six atom impls.
  Phase 8 → patch + review + auto-surface substrate landed; native
    UI mount in the web composer stays scheduled.

Changelog names every shipped surface so a reviewer can audit the
delta without reading the plan.

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 15:32:23 +00:00
Cursor AgentandTom Huang c65f9ccf7a feat(plugins): auto-derive choice surface for diff-review (Phase 8 entry slice)
Plan P3 / spec §10.3.1 / §21.5.

Mirrors the connector-gate's auto oauth-prompt pattern: when the
EFFECTIVE pipeline (consumer-declared OR scenario-fallback) contains
a stage whose atoms[] list `diff-review`, applyPlugin() now
synthesises an implicit `choice` GenUI surface so the user can
accept / reject / partial without the plugin author having to
declare the surface by hand.

apps/daemon/src/plugins/atoms/auto-surfaces.ts:

  deriveAutoAtomSurfaces({ pipeline }) → GenUISurfaceSpec[]

  For each stage that lists 'diff-review', emits one surface:

    id:      '__auto_diff_review_<stageId>'
    kind:    'choice'
    persist: 'run'                  (per-run; user sees one prompt
                                     per cycle through the stage)
    trigger: { stageId, atom: 'diff-review' }
    schema:  { decision: ['accept' | 'reject' | 'partial'],
               accepted_files: string[],
               rejected_files: string[],
               reason: string }
             required: ['decision']
    timeout:    24 h (a diff review may sit overnight)
    onTimeout:  'abort'

apply.ts integration:

  - Auto-surface derivation moved AFTER the bundled-scenario
    fallback resolution (P3 / O1) so a consumer that inherits
    the bundled scenario's diff-review stage still gets the
    auto-surface.
  - Plugin-author-declared surface with the same id wins, via
    the existing mergeAutoOAuthPrompts dedupe (re-used so we
    don't duplicate dedupe logic).

Daemon tests: 1608 → 1614 (+6 cases on plugins-auto-surfaces:
multi-stage diff-review surface fan-out, no-pipeline returns
empty, no-diff-review returns empty, plugin-declared pipeline
integration, scenario-fallback pipeline integration, plugin-
author-declared surface with same id wins).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 15:30:45 +00:00
Cursor AgentandTom Huang 8ecc7cbdb9 feat(plugins): figma-extract atom impl (Phase 6 entry slice)
Plan P2 / spec §10 / §21.3.1.

apps/daemon/src/plugins/atoms/figma-extract.ts ships the daemon-side
Figma REST shell-out behind the SKILL.md fragment landed in §3.M3.
Given a Figma file URL (or pre-extracted fileKey) + an OAuth token
the runner produces the spec §21.3.1 on-disk snapshot:

  <cwd>/figma/tree.json    flat node tree { id / name / type /
                           parent / children / box / fill / stroke /
                           cornerRadius / text / componentRef }
  <cwd>/figma/tokens.json  design-extract-shaped token bag so
                           token-map can crosswalk figma + code
                           sources through one helper
  <cwd>/figma/meta.json    fileUrl + fileKey + version +
                           lastModified + exportedAt + atomDigest +
                           unsupportedNodes[] + nodeCount

Public surface:

  runFigmaExtract({ cwd, fileUrl?, fileKey?, token, fetchFn?,
                    offlineAssets? })
    → FigmaExtractReport

URL parser accepts both legacy /file/<KEY> and /design/<KEY>
formats. fileKey supplied directly bypasses URL parsing.

Network is pluggable: tests pass a stub via fetchFn; production
defaults to globalThis.fetch. The OAuth token is forwarded as
Authorization: Bearer <t> and never persisted by the runner — the
daemon's connector-gate (spec §10.3.1 implicit oauth-prompt)
remains the only place that touches the token store.

Token lift heuristics (spec §21.3.1 anti-pattern: 'never make up
node ids'):
  - SOLID fills + strokes → colors[] with #RRGGBB(AA)
  - cornerRadius           → radius[] (e.g. 12px)
  - FRAME / GROUP heights  → spacing[] candidates
  - GRADIENT / IMAGE fills → meta.unsupportedNodes[] with reason
                             so the human can audit gaps
  - Component instances    → componentRef so token-map can
                             de-duplicate at the right boundary

#RRGGBBAA emitted only when alpha < 1.

Daemon tests: 1600 → 1608 (+8 cases on plugins-figma-extract:
flat-tree walk + parent/box/fill capture, multi-kind token lift
(color + radius + spacing), persisted on-disk layout + meta digest,
gradient fill recorded in unsupportedNodes, fileKey override skips
URL parsing + bearer header forwarded, missing fileKey error,
missing token error, non-2xx surfaces upstream status text).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 15:27:30 +00:00
Cursor AgentandTom Huang bda5c0e3ed feat(plugins): token-map atom impl (Phase 6/7 entry slice)
Plan P1 / spec §10 / §21.3.1.

apps/daemon/src/plugins/atoms/token-map.ts ships the daemon-side
crosswalk behind the SKILL.md fragment landed in §3.M3. Given a
project cwd that already has a token bag (`code/tokens.json` from
design-extract OR `figma/tokens.json` from figma-extract) plus the
active design system's token vocabulary, the runner produces:

  <cwd>/token-map/colors.json
  <cwd>/token-map/typography.json
  <cwd>/token-map/spacing.json
  <cwd>/token-map/radius.json
  <cwd>/token-map/shadow.json
  <cwd>/token-map/unmatched.json     [{ source, sourceName?, kind, reason, hint? }]
  <cwd>/token-map/meta.json          { sourceKind, generatedAt,
                                       atomDigest, designSystemId?,
                                       targetTokenCount,
                                       sourceTokenCount,
                                       matchedTokenCount }

Match strategies (deterministic, first-win):

  1. Exact value match  (#5b8def === #5b8def).
  2. Normalised hex     (#abc → #aabbcc, ignores case).
  3. Fuzzy name         (--color-primary → --ds-color-primary; strips
                         leading -- / ds- / odds- / theme- prefixes
                         and lower-cases).

Anything else lands unmatched[] with one of:

  - 'no-target-equivalent'  no target with same value/name
  - 'target-collision'      multiple sources mapped to one target
                            (first wins; subsequent listed unmatched
                             with hint pointing at the first claimant)
  - 'invalid-source'        malformed source value

Strict mode aborts on any unmatched; soft mode (default) populates
unmatched.json + continues so the human can audit.

Helper:

  parseDesignSystemTokens(body) → DesignSystemToken[]

  Heuristic DESIGN.md token extraction. Lifts CSS custom properties
  (`--ds-color-fg: #111`) and markdown table rows that look like
  token declarations. Daemon callers may pass a hand-curated bag
  instead of relying on this parser.

Existing patch-edit typecheck noise fixed in passing:
  - PatchStepRecord now extends Omit<RewriteStep, 'rationale'> so
    optional override is sound under exactOptionalPropertyTypes.
  - rationale assignment guarded against undefined.

Daemon tests: 1588 → 1600 (+12 cases on plugins-token-map: exact
match, normalised hex, fuzzy name, no-target-equivalent, target-
collision, strict mode abort, multi-kind crosswalk, code+figma
disk inputs round-trip + bucket persistence + missing-input error,
DESIGN.md CSS custom property + markdown table parsers).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 15:24:22 +00:00
Cursor AgentandTom Huang 01e94a92be docs(plugins): record O-series landing in living plan + CHANGELOG
Plan O6 — sync docs/plans/plugins-implementation.md and CHANGELOG.md
with the O-series shipped this turn:

  O1. Bundled-scenario pipeline fallback resolver (spec §23.3.3)
  O2. design-extract atom impl                    (Phase 6/7 entry slice)
  O3. rewrite-plan atom impl                      (Phase 7 entry slice)
  O4. patch-edit atom impl                        (Phase 7 entry slice)
  O5. diff-review atom impl                       (Phase 7-8 entry slice)

Living plan now reads:
  Phase 7 → ALL SIX atoms IMPLEMENTED (build-test + code-import +
    design-extract + rewrite-plan + patch-edit + diff-review).
    Bundled scenario plugin od-code-migration ships the canonical
    pipeline. Live HTTP wiring for the per-stage runner stays
    scheduled.
  Phase 8 → patch + review substrate landed; native review-and-apply
    UI mounting diff-review through a GenUI choice surface stays
    scheduled.
  Phase 6 → SKILL.md substrate + scenario plugin registered;
    design-extract impl is reused under the figma-migration
    flow as well.

Changelog names every shipped surface so a reviewer can audit the
delta without reading the plan.

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 15:19:04 +00:00
Cursor AgentandTom Huang 4687f9c060 feat(plugins): diff-review atom impl (Phase 7-8 entry slice)
Plan O5 / spec §20.3 / §21.3.2.

apps/daemon/src/plugins/atoms/diff-review.ts ships the daemon-side
impl behind the SKILL.md fragment landed in §3.M4. The runner walks
plan/receipts/ + plan/steps.json and emits the four files the
SKILL.md fragment promises:

  <cwd>/review/diff.patch     receipt-derived patch concat
  <cwd>/review/summary.md     per-step walkthrough w/ +/- stats
  <cwd>/review/decision.json  accept / reject / partial
  <cwd>/review/meta.json      generatedAt + atomDigest +
                              planRevision

Public surface:

  runDiffReview({ cwd, decision? }) → DiffReviewReport
    { files[], added, removed, receipts[], decision?, meta }

Decision composition rules (spec §20.3):
  - 'accept'  → accepted_files defaults to every touched file;
                rejected_files defaults to [].
  - 'reject'  → rejected_files defaults to every touched file;
                accepted_files defaults to [].
  - 'partial' → MUST cover every touched file via the union of
                accepted_files + rejected_files; otherwise the
                runner throws 'missing <file>' so the GenUI surface
                can re-prompt.

Round trip semantics: when decision is omitted, the runner reads
review/decision.json from disk if it exists and returns it on the
report so a subsequent stage can inspect the user's prior choice
without re-prompting.

The diff.patch artefact is intentionally a receipt-derived summary
(file list + added/removed counts + rationale comments). Precise
hunks live in plan/receipts/<id>.json — the SKILL.md fragment
spells this contract out so the agent doesn't claim diff.patch is
git-apply-shaped.

Daemon tests: 1581 → 1588 (+7 cases on plugins-diff-review:
artefact emission from receipts, aggregation + dedupe across multi-
file receipts, accept default-fills-all, reject default-fills-all,
partial requires full coverage, decision round-trip from disk,
empty-receipts dir non-crash).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 15:16:53 +00:00
Cursor AgentandTom Huang 561da49019 feat(plugins): patch-edit atom impl (Phase 7 entry slice)
Plan O4 / spec §20.3 / §21.3.2.

apps/daemon/src/plugins/atoms/patch-edit.ts ships the daemon-side
unified-diff applier behind the SKILL.md fragment landed in §3.M4.

Public surface:

  applyPatchForStep({ cwd, stepId, diff, rationale?, allowOutOfStepCreation? })
    → { status: 'completed' | 'failed' | 'skipped',
        filesTouched, added, removed, reason? }

  skipStep({ cwd, stepId, rationale })   — explicit skip path
  readPlanProgress(cwd)                  — { total, terminal } for
                                            the pipeline runner's
                                            until expression
                                            'plan.steps.terminal === plan.steps.total'

Diff parsing supports:

  - plain edits with @@ headers and ' '/-/+ body lines
  - file creation via '--- /dev/null' + '+++ b/<path>'
  - file deletion via '--- a/<path>' + '+++ /dev/null'
  - multiple file hunks in one diff (split on '--- ')

Safety contract enforced before any file is written:

  1. Path traversal guard ('..', leading '/').
  2. Every touched file MUST be in step.files[]. Out-of-step file
     creation requires allowOutOfStepCreation=true (test harness
     escape hatch).
  3. Touching a file classified as 'shell' tier in
     plan/ownership.json requires step.risk='high'. Spec §11.5.1
     boundary: layout.tsx / theme.ts / globals.css / providers.tsx
     can't be edited under risk='low'/'medium'.
  4. Context lines must match exactly; stale patches surface
     'context mismatch at line N' WITHOUT mutating disk.

After a successful apply, the runner:

  - sets plan/steps.json[id].status='completed' + completedAt
  - writes plan/receipts/step-<id>.json with files[], added, removed,
    rationale, completedAt

skipStep() and 'already in terminal state' returns are idempotent
so the pipeline runner can safely re-call after a crash.

Daemon tests: 1571 → 1581 (+10 cases on plugins-patch-edit:
single-file edit + receipt persistence, /dev/null file creation,
/dev/null file deletion, off-step file rejection, shell-tier risk
gate, path-traversal rejection, stale-patch context mismatch
rollback, idempotent terminal-state return, skipStep + receipt,
readPlanProgress aggregate).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 15:14:09 +00:00
Cursor AgentandTom Huang f2b62c0303 feat(plugins): rewrite-plan atom impl (Phase 7 entry slice)
Plan O3 / spec §20.3 / §21.3.2.

apps/daemon/src/plugins/atoms/rewrite-plan.ts produces the four
files the SKILL.md fragment promises:

  <cwd>/plan/plan.md          — narrative (heuristic baseline; LLM
                                step overwrites)
  <cwd>/plan/ownership.json   — { file, layer } per file across the
                                imported repo, sorted lexicographically
  <cwd>/plan/steps.json       — ordered { id, files[], rationale, risk }
  <cwd>/plan/meta.json        — generatedAt + atomDigest +
                                tokenMapDigest + intent

Ownership classifier (spec §11.5.1 / §20.3 tiers):

  shell  layout.tsx / _app.tsx / _document.tsx / providers.tsx /
         theme.ts / globals.css / tokens.css / design-tokens.css
  route  app/.../page.tsx, pages/index.tsx, route.ts entries
  shared hooks/ lib/ utils/ providers/ context/ store/ stores/
         services/ api/ shared/ common/, plus repo-root config files
  leaf   everything else (default)

The classifier keeps shell rare so patch-edit's safety gate doesn't
lock the agent out of plain component edits.

Step generator:
  - tokens-alignment   (low risk) when design-extract surfaced
                       inline literals; files[] is the leaf set
                       carrying those literals.
  - rewrite-<slug>     (low risk) one per leaf component file;
                       sibling stylesheets (Button.tsx + Button.css)
                       fold into the same step.
  - shared-and-route-touchups (medium risk) when shared/route files
                       exist, marks the cross-cutting boundary.
  - build-test         (low risk) closes every plan; rewrite-plan
                       refuses to drop this step.

atomDigest is over a canonicalised view of code/index.json so re-walks
that don't change the file roster don't invalidate the plan; intent is
not in the digest. tokenMapDigest is over code/tokens.json or 'none'.

Daemon tests: 1564 → 1571 (+7 cases on plugins-rewrite-plan: tier
classification across leaf/shared/route/shell, tokens-alignment
emission, per-leaf rewrite step + sibling stylesheet bundling,
build-test always last, persisted file layout, digest stability
across runs, missing code/index.json error path).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 15:10:47 +00:00
Cursor AgentandTom Huang c17d4ab952 feat(plugins): design-extract atom impl (Phase 6/7 entry slice)
Plan O2 / spec §10 / §21.3.2.

apps/daemon/src/plugins/atoms/design-extract.ts ships the
daemon-side implementation behind the SKILL.md fragment landed in
§3.M4 / §3.N. Given a project cwd that already has
`code/index.json` (from code-import) and the imported repo path,
the runner walks every scannable file (css/scss/ts/tsx/js/jsx/
html/json) and extracts:

  colors      — hex (#abc / #aabbcc / #aabbccdd), rgba(), hsla(),
                CSS custom properties (--*-color / --*-bg / etc),
                Tailwind config quoted hex palette entries.
  typography  — font-family declarations.
  spacing     — px / rem / em values on padding/margin/gap/inset/
                top/left/right/bottom.
  radius      — border-radius declarations.
  shadow      — box-shadow declarations.

Each token is deduped by canonical value and carries:
  - sources[]: '${path}:${line}' entries (audit trail token-map
    references).
  - usage[]:   bare basenames so a designer can spot 'this colour
    is referenced from Header.tsx + Footer.tsx + button.css'.
  - name?:     populated for CSS custom properties; absent for
    inline literals.

The pass is heuristic by design: false negatives are preferable to
false positives because token-map asks the human to confirm each
match. Files larger than 256 KiB are skipped (default; configurable)
to keep the regex pass bounded on bundled output.

Output: `<cwd>/code/tokens.json` — the exact shape the SKILL.md
fragment promises.

Daemon tests: 1556 → 1564 (+8 cases on plugins-design-extract:
hex/rgba/CSS-variable colour extraction, font-family capture,
spacing extraction across px+rem, border-radius + box-shadow,
Tailwind config quoted hex palette, persisted JSON layout, missing
code/index.json error path, empty-bag round trip).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 15:08:11 +00:00
Cursor AgentandTom Huang 694289295a feat(plugins): wire bundled-scenario pipeline fallback through apply (spec §23.3.3)
Plan O1 / spec §23.3.3.

When a plugin omits `od.pipeline`, applyPlugin() now consults the
bundled scenario plugins registered by the daemon's bundled boot
walker (apps/daemon/src/plugins/bundled.ts) and copies the matching
scenario's pipeline verbatim into the snapshot. Match key:
`od.taskKind` (defaulting to 'new-generation' when absent).

Pure-side (packages/plugin-runtime):

  - new resolveAppliedPipeline({ manifest, scenarios }) →
      { pipeline, source: 'declared' | 'scenario' | 'none', scenarioId? }
  - RegistryView gains optional `scenarios?: ScenarioRegistryEntry[]`
    so the daemon registry view propagates the lookup table without
    coupling the pure resolver to SQLite.

Daemon-side (apps/daemon):

  - applyPlugin() calls resolveAppliedPipeline and stores the result on
    BOTH `ApplyResult.pipeline` and `AppliedPluginSnapshot.pipeline`
    so a replay reproduces the same stages without re-consulting the
    scenario registry.
  - server.loadPluginRegistryView() now collects bundled scenarios
    from `installed_plugins` (source_kind='bundled', od.kind='scenario',
    non-empty pipeline). Third-party scenarios are intentionally NOT
    eligible — only rows seeded by the bundled boot walker.

Invariants preserved:
  - Apply stays pure: scenarios are read once at the registry-view
    construction site and passed in; apply never opens a DB itself.
  - Scenario plugins themselves never fall back to themselves
    (resolveAppliedPipeline returns 'none' when od.kind='scenario').
  - manifestSourceDigest is unaffected by the fallback (digest is over
    manifest + inputs + resolvedContextRefs); replay invariance held.

Tests: +15 (plugin-runtime: 7 cases on resolveAppliedPipeline;
daemon: 8 cases on plugins-scenario-fallback covering declared
pipeline win, scenario fallback by taskKind, default to
new-generation, scenario plugins don't recurse, no scenarios →
pipeline=undefined, manifestSourceDigest stability across applies,
DB collector round-trip).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 15:05:54 +00:00
Cursor AgentandTom Huang 1d142b4214 docs(plugins): record N-series landing in living plan + CHANGELOG
Plan §3.N5 — sync docs/plans/plugins-implementation.md and
CHANGELOG.md with the N-series shipped this turn:

  N1. build-test atom shell-out runner (Phase 7 entry slice)
  N2. code-import atom repo walker      (Phase 7 entry slice)
  N3. handoff helper + ArtifactManifest provenance fields
                                        (Phase 7-8 entry slice)
  N4. Bundled scenario plugins          (spec §23.3.3)

Living plan now reads:
  Phase 7 → build-test + code-import IMPLEMENTED; design-extract +
    rewrite-plan + patch-edit + diff-review remain SKILL.md
    substrate.
  Phase 8 → handoff helper + provenance fields IMPLEMENTED; native
    review-and-apply UI remains scheduled.
  Phase 6 → SKILL.md substrate + scenario plugin registered;
    Figma REST + token-map heuristic remain scheduled.

Changelog names every shipped surface so a reviewer can audit the
delta without reading the plan.

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 14:57:23 +00:00
Cursor AgentandTom Huang cfdc79f859 feat(plugins): bundled scenario plugins (spec §23.3.3)
Plan N4 / spec §10.1 / §23.3.3.

Lands one bundled `od.kind: 'scenario'` plugin per taskKind enum
value under plugins/_official/scenarios/. The daemon's bundled
boot walker (§3.I3) already registers them via the existing
'tier/<id>' layout, so no changes to bundled.ts.

Four scenarios:

  od-new-generation  discovery → plan → generate → critique (devloop)
  od-figma-migration extract  → tokens → generate → critique (devloop)
                     declares od.connectors.required: [{id:'figma'}]
                     so the daemon auto-derives the figma-oauth
                     surface at apply time
  od-code-migration  import   → tokens → plan
                     → verify (patch-edit ↔ build-test devloop, until
                       'build.passing && tests.passing || iterations>=8')
                     → review → handoff
  od-tune-collab     direction → patch → critique (devloop) → handoff

Locks the spec §23.3.3 patch 3 contract: 'no od.pipeline + has
taskKind → look up the bundled scenario plugin'. The daemon-side
resolver wiring is the next PR; the substrate is in place so a
plugin author who omits the pipeline gets a deterministic default.

Daemon tests: 1543 → 1548 (+5 cases on
plugins-bundled-scenarios-roster: roster shape + per-scenario
taskKind / kind / pipeline-stage-id assertions). The atom roster
test from §3.M3+M4 stays focused on plugins/_official/atoms/.

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 14:52:59 +00:00
Cursor AgentandTom Huang e6eaa62294 feat(plugins): handoff atom + ArtifactManifest provenance fields
Plan N3 / spec §11.5.1 / §21.5.

@open-design/contracts ArtifactManifest gains the spec §11.5.1
provenance + downstream-distribution surface as additive optional
fields:

  sourcePluginSnapshotId / sourcePluginId / sourcePluginVersion /
    sourceTaskKind / sourceRunId / sourceProjectId / parentArtifactId
  artifactKind / renderKind / handoffKind
  exportTargets[] / deployTargets[]

Spec §11.5.1 invariants:
  - sourcePluginSnapshotId NEVER changes after first write.
  - exportTargets[] / deployTargets[] are append-only.
  - handoffKind promotes monotonically along
    design-only < implementation-plan < patch < deployable-app.

apps/daemon/src/plugins/atoms/handoff.ts ships the daemon-side
helper:

  recordHandoff({ manifest, exportTarget?, deployTarget?,
                  handoffKind?, enforceMonotonicHandoff? })
    → { manifest, changed }

  - Idempotent: a (surface, target) pair only ever lands once on
    exportTargets[]; same for (provider, location) on deployTargets[].
  - handoffKind defaults to monotonic; pass enforceMonotonicHandoff:
    false on a rollback path.

  isDeployableAppEligible({ manifest, buildPassing, testsPassing })
    → boolean

  Spec §11.5.1 promotion rule for the deployable-app tier: requires
  build.passing + tests.passing AND at least one exportTargets[]
  entry on docker / cli surface. Centralises the rule so plugins
  don't reimplement it.

packages/contracts/src/index.ts now uses .js extensions on every
re-export so the daemon's NodeNext moduleResolution picks up the
new types end-to-end.

Daemon tests: 1534 → 1543 (+9 cases on plugins-handoff: appends
exportTargets / deployTargets, idempotency, monotonic handoffKind
promotion, downgrade refusal vs. rollback escape, deployable-app
eligibility rule).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 14:48:29 +00:00
Cursor AgentandTom Huang 0ab7ecf403 feat(plugins): code-import atom repo walker (Phase 7 entry slice)
Plan N2 / spec §10 / §21.3.2 / §16 Phase 7.

apps/daemon/src/plugins/atoms/code-import.ts ships the daemon-side
implementation of the SKILL.md fragment landed in §3.M4. Given a
repo path + a project cwd, the runner walks the tree once, writes
`<cwd>/code/index.json`, and never re-walks until the next
explicit re-import.

The walker:
  - Honours OD_CODE_IMPORT_BUDGET_MS (default 60s) and stops cleanly
    when the budget is exhausted, recording every unwalked
    directory under skipped[] with reason='budget-exceeded'.
  - Skips the canonical no-go folders: node_modules, .git, .next,
    .svelte-kit, dist, build, out, coverage, .vercel, .vscode,
    .turbo, .cache, .pnpm-store, .parcel-cache, .nuxt, .astro.
    Each entry lands in skipped[] with reason='directory-skiplist'.
  - Marks symlinks as skipped (reason='symlink') without following.
  - Records files larger than largeFileBytes (default 1 MiB) in
    skipped[] AND in files[] (so the agent sees they exist), but
    omits their imports[] to keep memory bounded.
  - Detects framework: next (app vs pages router), sveltekit, astro,
    remix, vite, cra, custom, unknown.
  - Detects package manager from lockfile presence (pnpm-lock.yaml
    > yarn.lock > bun.lockb > package-lock.json > unknown).
  - Detects style system: tailwind / styled-components / emotion /
    css.
  - Lightweight import edge extraction (regex) for ts/tsx/js/jsx;
    documented as heuristic in the SKILL.md fragment.

Daemon tests: 1528 → 1534 (+6 cases on plugins-code-import:
next-app router + tailwind + pnpm detection, skiplist enforcement,
symlink skip, large-file imports omission, persisted JSON layout,
non-directory repoPath rejection).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 14:33:53 +00:00
Cursor AgentandTom Huang 5b6a0899ce feat(plugins): build-test atom shell-out runner (Phase 7 entry slice)
Plan N1 / spec §10 / §22.4 / §16 Phase 7.

apps/daemon/src/plugins/atoms/build-test.ts ships the daemon-side
implementation that the bundled plugins/_official/atoms/build-test
SKILL.md fragment locks the contract for:

  runBuildTest({ cwd, buildCommand?, testCommand?, ... }) → BuildTestReport

The runner:
  - Honours explicit overrides (highest priority).
  - Otherwise infers commands from package.json scripts:
      typecheck (or build) → `{pm} run typecheck`
      test                  → `{pm} run test`
  - Auto-picks the package manager from lockfile presence
    (pnpm-lock.yaml → pnpm; yarn.lock → yarn; else npm).
  - Skips a half cleanly when no command resolved, recording an
    explicit reason. Skipped doesn't flip the matching signal off
    (a project that opts out of typecheck still gets build.passing=true).
  - Caps combined stdout+stderr at 1 MiB by default, marks
    truncated output with '…[truncated]'.
  - Per-command timeout 5min default, sends SIGTERM at expiry.
  - Spawn function is pluggable so callers can inject a stub.

writeBuildTestReport() persists the canonical critique/build-test.json
+ critique/build-test.log layout the SKILL.md fragment promises.

UntilSignals gains 'build.passing' + 'tests.passing' so a plan can
write `until: 'build.passing && tests.passing'` directly. Closes
the spec §22.4 limit-1 gap that was blocking scenario 2 (code
migration) from natively expressing 'build/test pass = converged'.

The score axis (legacy critique.score) maps:
  both pass        → 5
  only build pass  → 3
  both fail        → 1
so existing pipelines that read critique.score keep working.

Daemon tests: 1521 → 1528 (+7 cases on plugins-build-test:
both-pass, build-fail, skip-with-reason, log truncation, npm
script inference, pnpm preference, JSON + log persistence).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 14:30:36 +00:00
Cursor AgentandTom Huang 4aec507cd3 docs(plugins): record M-series — retention TTL + atom prompt rewiring + Phase 6/7/8 substrate
CHANGELOG.md ## Unreleased gets a new bullet block summarising:
  - OD_SNAPSHOT_RETENTION_DAYS referenced-row TTL (PB2 closed).
  - OD_BUNDLED_ATOM_PROMPTS=1 activates composeDaemonSystemPrompt's
    atom-block branch (default off; byte-equal to today's prompt).
  - Nine new bundled atom SKILL.md fragments under
    plugins/_official/atoms/ for Phases 6, 7, and 8.

docs/plans/plugins-implementation.md §6 Phase 6 / 7 / 8 deliverables
record their substrate-shipped state; the actual implementations
remain scheduled. §9 Status snapshot updated; next-planned-PR cell
moves the AWS SDK / postgres adapter wiring + Phase 6 / 7 / 8
shell-out implementations + the OD_BUNDLED_ATOM_PROMPTS default
flip to the head of the queue.

Final gates: pnpm guard ✓, daemon 1521 tests ✓, web 586 tests ✓,
contracts 12 tests ✓, agui-adapter 9 tests ✓, plugin-runtime 21.

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 14:25:22 +00:00
Cursor AgentandTom Huang 030d5ce7b2 feat(plugins): Phase 6 + 7 + 8 atom SKILL.md substrate
Plan M3 + M4 / spec §21.4 / §23.3.2.

Lands the prompt-fragment substrate for every atom spec §10
reserves under (planned) ids. The directory now ships nine new
atom plugins under plugins/_official/atoms/, each a SKILL.md +
open-design.json pair the daemon's bundled boot walker registers
on startup:

  Phase 6 (figma-migration native):
    figma-extract  pull node tree + tokens + assets into project cwd
    token-map      crosswalk source tokens onto active DESIGN.md

  Phase 7 (code-migration native):
    code-import    normalise repo into code/index.json snapshot
    design-extract lift tokens out of source / Figma / screenshots
    rewrite-plan   author the multi-file plan with ownership tiers
    patch-edit     execute one plan step per turn with ownership guards
    diff-review    surface unified diff + capture accept/reject
    build-test     run framework-default test commands; emit
                   build.passing + tests.passing signals

  Phase 8 (production code delivery):
    handoff        push artifact to cli/desktop/web/docker/github/
                   figma/code-agent surfaces; append to
                   ArtifactManifest.exportTargets[]

The implementations stay deliberately prose-only in v1 — the
fragments teach the agent what each atom expects, the spec §22.5
promotion path covers iterating each one out-of-tree first. The
matching FIRST_PARTY_ATOMS catalog rows already mark them
'planned'; today's commit gives the bundled boot walker something
to register so a plugin author who references one of these ids in
od.pipeline.stages[*].atoms[] sees the SKILL.md inside the
container without the doctor warning.

apps/daemon/tests/plugins-bundled-atoms-roster.test.ts (2 cases)
pins the on-disk inventory so a future PR can't drop or rename an
atom folder without touching the spec §10 / §21 / §23 tables in
the same patch.

Daemon tests: 1519 → 1521 (+2 cases on the new roster suite).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 14:22:18 +00:00
Cursor AgentandTom Huang d491b3b7da feat(plugins): wire activeStageBlocks under OD_BUNDLED_ATOM_PROMPTS=1
Plan M2 / spec §23.4 / §23.3.2 patch 2.

Closes the runtime half of the §23.3.2 lift. When
OD_BUNDLED_ATOM_PROMPTS=1 is set AND the run carries an
appliedPluginSnapshotId whose snapshot has a non-empty
od.pipeline.stages[*], composeDaemonSystemPrompt now:

  1. Fetches the snapshot from applied_plugin_snapshots.
  2. For each stage, calls loadAtomBodies(db, stage.atoms ?? []).
  3. Calls renderActiveStageBlock({ stageId, bodies }) for each
     stage; skips blocks that come back empty (no bundled atom
     resolved).
  4. Threads the resulting array as composeSystemPrompt's
     activeStageBlocks, which the composer splices in immediately
     after the  block.

Default behaviour (flag unset OR no snapshot OR no pipeline) is
byte-equal to today's prompt. Errors during atom-body loading
surface as a single console.warn and never block the run; the
composer falls through to the inline DISCOVERY_AND_PHILOSOPHY
constants.

The run's appliedPluginSnapshotId is forwarded from the in-memory
run object set up by POST /api/runs (plan §3.A1). Non-plugin runs
pass undefined and skip the entire branch.

Daemon tests stay at 1519/1519 (the new branch is opt-in; existing
tests don't set the env flag, so the default path is exercised
unchanged. The activeStageBlocks composer field already has 3
unit tests from §3.L2.)

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 14:12:04 +00:00
Cursor AgentandTom Huang 1fdda937f7 feat(plugins): OD_SNAPSHOT_RETENTION_DAYS referenced-row TTL (PB2)
Plan M1 / spec PB2 / §16 Phase 5.

Closes the 'expire even referenced rows' knob spec PB2 reserved as
operator-opt-in. When OD_SNAPSHOT_RETENTION_DAYS is set,
pruneExpiredSnapshots additionally retires snapshot rows whose
project_id no longer exists in the projects table AND whose
applied_at is older than the configured window. Live-project rows
stay pinned forever (reproducibility wins).

The 'project deleted' check is the v1 stand-in for the spec's
'referencing run/conversation/project is itself terminal' clause:
runs are in-memory in v1 (no SQLite signal we can read), and
conversations have no archived_at column. Deleting a project is
the loudest 'this is over' signal in v1, and it's the failure mode
that actually accumulates dangling snapshot rows in practice.

apps/daemon/src/plugins/gc.ts threads
readPluginEnvKnobs().snapshotRetentionDays into both the periodic
tick and the synchronous sweep() handle so the operator escape
hatch (od plugin snapshots prune --before <ts>) and the worker
share one rule.

Daemon tests: 1516 → 1519 (+3 cases on plugins-snapshot-gc:
deleted-project + old-enough-row pruned, deleted-project + recent
row survives, live-project + ancient row never pruned).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 14:07:02 +00:00
Cursor AgentandTom Huang 35b4030834 docs(plugins): record L-series — Helm overrides + activeStageBlocks + asset route + storage substrate
CHANGELOG.md ## Unreleased gets a new bullet block summarising:
  - Per-cloud Helm value overrides (AWS / GCP / Azure / Aliyun /
    Tencent / Huawei / self-hosted).
  - composeSystemPrompt accepts activeStageBlocks so a future PR can
    swap inline DISCOVERY_AND_PHILOSOPHY for SKILL.md fragments.
  - Plugin-bundled component surface (sandboxed iframe) +
    GET /api/plugins/:id/asset/* with §9.2 CSP.
  - ProjectStorage + DaemonDb adapter substrate (Local impl wired,
    S3 + postgres stubs interface-locked).

docs/plans/plugins-implementation.md §3 / §6 / §9 flipped:
  - Phase 5 deliverables: chart templates + per-cloud overrides +
    bound-API-token guard + ghcr CI + ProjectStorage + DaemonDb all
    record their landed shipped state.
  - §6 Phase 4 absorbs the activeStageBlocks composer field +
    surface.component sandbox loader as shipped slices.
  - §9 Status snapshot updated; head of the queue is now the live
    composeSystemPrompt rewiring + AWS SDK / postgres adapter
    wiring + OD_SNAPSHOT_RETENTION_DAYS referenced-row TTL +
    Phase 6/7/8 native scenarios.

Final gates: pnpm guard ✓, daemon 1516 tests ✓, web 586 tests ✓,
contracts 12 tests ✓, agui-adapter 9 tests ✓, plugin-runtime 21.

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 14:01:29 +00:00
Cursor AgentandTom Huang 0c2c2bb0c7 feat(plugins): ProjectStorage + DaemonDb adapter substrate (Phase 5)
Plan L4 / spec §15.6.

apps/daemon/src/storage/project-storage.ts ships:

  - ProjectStorage interface (read / write / list / delete / stat).
  - LocalProjectStorage (v1 default; backed by fs/promises with the
    same traversal guards the legacy projects.ts helpers enforce).
  - S3ProjectStorage stub — interface-locked; throws StorageError(IO)
    on every operation until the AWS SDK wiring lands. The stub still
    exposes keyFor() so tests can lock the bucket-prefix join.
  - resolveProjectStorage({ projectsRoot, env }) reads
    OD_PROJECT_STORAGE / OD_S3_BUCKET / OD_S3_REGION / OD_S3_PREFIX /
    OD_S3_ENDPOINT and returns the matching backend.

apps/daemon/src/storage/daemon-db.ts ships:

  - resolveDaemonDbConfig({}) returns { kind: 'sqlite' } by default.
  - With OD_DAEMON_DB=postgres + OD_PG_HOST/DATABASE/USER (and
    optional OD_PG_PORT / OD_PG_SSL_MODE), returns a postgres
    configuration object the future adapter will consume.
  - Throws DaemonDbConfigError on missing required env vars or
    unknown OD_DAEMON_DB values.

Neither adapter is wired into the daemon's existing project routes
yet — the substrate slice keeps the call sites unchanged so a wrong
adapter never silently corrupts user data on roll-out. The next
Phase 5 PR routes apps/daemon/src/projects.ts through the resolved
ProjectStorage and lands the real AWS SDK + Postgres impls behind
these interfaces.

Daemon tests: 1503 → 1516 (+13 cases on storage.test.ts:
LocalProjectStorage round-trip + nested list + traversal rejection +
NOT_FOUND, S3 stub key-builder + throw-on-op + bucket/region guard,
resolver default + s3 + DaemonDb sqlite default + postgres parsing +
incomplete env + unknown kind).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 13:58:27 +00:00
Cursor AgentandTom Huang 61931faef9 feat(plugins): plugin-bundled component surface (sandboxed iframe)
Plan L3 / spec §10.3.5 / §9.2.

Web: GenUISurfaceRenderer now renders a sandboxed iframe when the
surface declares a component path. Communication is one-way: the
iframe posts `{ kind: 'genui:respond', surfaceId, value }` envelopes
back to the parent via window.postMessage; the parent forwards to
onAnswered. The iframe carries `sandbox="allow-scripts"` only — no
allow-same-origin / forms / popups / downloads, exactly as spec §9.2
mandates.

PendingSurface gains `componentPluginId`, supplied by the host (from
the run's AppliedPluginSnapshot.pluginId). Surfaces missing that
field render an inline error so a misconfigured host fails loudly.

Daemon: GET /api/plugins/:id/asset/* serves arbitrary files inside an
installed plugin's fsPath under the §9.2 preview CSP:

  Content-Security-Policy: default-src 'none'; img-src 'self' data: blob:;
                           media-src 'self' data: blob:;
                           style-src 'self' 'unsafe-inline';
                           script-src 'self' 'unsafe-inline';
                           connect-src 'none'; frame-ancestors 'self'
  X-Content-Type-Options: nosniff

Three guards: unknown plugin id → 404, traversal segments / absolute
paths → 400, escape-via-resolved-path → 400. Content-Type maps
common asset extensions; everything else falls back to
application/octet-stream so the browser's nosniff respects it.

Daemon tests: 1499 → 1503 (+4 cases on plugins-asset-route covering
404 / traversal-rejection / 200 with CSP headers / asset-not-found).
Web typecheck clean; jsdom tests stay at 586/586 (the iframe path is
DOM-shape-locked and has no callable surface to drive without a
real browser).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 13:55:04 +00:00
Cursor AgentandTom Huang dfbb646b5d feat(plugins): composeSystemPrompt accepts activeStageBlocks (spec §23.4)
Plan L2 / spec §23.3.2 patch 2 / §23.4.

Both composers (daemon-side apps/daemon/src/prompts/system.ts and
contracts-side packages/contracts/src/prompts/system.ts) now accept
an optional `activeStageBlocks` field on ComposeInput. When the
field is present the composer splices every non-empty block in
order, immediately after the `## Active plugin` block and before
the metadata block. Empty / whitespace-only entries are skipped so
a pipeline whose stages don't resolve any bundled atom bodies
produces zero extra prompt mass.

This is the substrate slice the §23.3.2 lift needs:

  - Inputs: activeStageBlocks: ReadonlyArray<string> | undefined
  - Producers: pipelineRunner / pre-run hook can build each block
    from `renderActiveStageBlock(stageId, loadAtomBodies(db, atoms))`
    (both helpers shipped in plan §3.J3).
  - Effect: when OD_BUNDLED_ATOM_PROMPTS is set (the next PR), the
    pipeline runner stamps the run's snapshot pipeline through the
    pair and feeds the assembled blocks to composeSystemPrompt;
    until then the field stays undefined and the prompt is byte-equal
    to today's output.

The hard-coded DISCOVERY_AND_PHILOSOPHY constant in system.ts stays
in place for v1 — replacing it would swap the entire prompt floor
on every run, which needs the bundled-atom SKILL.md bodies to fully
cover the discovery + critique workflow first. This commit only
adds the splice point.

Daemon tests: 1496 → 1499 (+3 cases: order-preserving multi-stage
splice, empty-block skip, no-op when undefined / empty).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 13:50:36 +00:00
Cursor AgentandTom Huang 2230b5767c feat(deploy): per-cloud Helm value overrides (spec §15.5)
Plan L1.

Seven values-<cloud>.yaml files at tools/pack/helm/open-design/:

  values-aws.yaml      EFS + gp3 + ALB ingress
  values-gcp.yaml      Filestore + pd-balanced + GCLB ingress
  values-azure.yaml    azurefile-csi + managed-csi + App Gateway ingress
  values-aliyun.yaml   alicloud-nas + alicloud-disk-essd
  values-tencent.yaml  cfs + cbs
  values-huawei.yaml   sfs-turbo + csi-disk
  values-self.yaml     cluster-default storageClass; ingress off

Operators install with:

  helm install od ./tools/pack/helm/open-design \
    -f tools/pack/helm/open-design/values-aws.yaml \
    --set secrets.apiToken=$(openssl rand -hex 32)

Each override is intentionally minimal — it only differs from the
baseline values.yaml on the volume + ingress dimensions the matching
cloud needs. Secret-manager wiring (External Secrets Operator,
secrets-store CSI) is documented inline; the chart's Secret stays
the destination of the sync.

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 13:45:53 +00:00
Cursor AgentandTom Huang d0dbc7314f docs(plugins): record K-series — bound-API-token + Helm + surface.component + ghcr CI
CHANGELOG.md ## Unreleased gets a new bullet block summarising:
  - Phase 5 bound-API-token guard (startServer refusal + bearer
    middleware on /api/*).
  - Helm chart templates (Deployment / Service / Secret / ConfigMap /
    PVCs / Ingress / NOTES).
  - od.genui.surfaces[].component manifest field + capability gate.
  - GitHub Actions multi-arch image push to ghcr.io.

docs/plans/plugins-implementation.md §6 / §9 flipped:
  - Phase 5 bound-API-token guard, CI push, Helm templates → shipped.
  - Phase 4 surface.component manifest upgrade → shipped (substrate;
    web sandbox loader stays scheduled).
  - §9 Status snapshot updated; head of the queue is now the
    composeSystemPrompt rewiring + the web custom-component sandbox
    loader + ProjectStorage S3 adapter + DaemonDb Postgres adapter +
    per-cloud values-<cloud>.yaml override files + Phase 6 / 7 / 8.

Final gates: pnpm guard ✓, daemon 1496 tests ✓, web 586 tests ✓,
contracts 12 tests ✓, agui-adapter 9 tests ✓, plugin-runtime 21 tests ✓.

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 13:42:54 +00:00
Cursor AgentandTom Huang 708f37dddb feat(deploy): GitHub Actions workflow for multi-arch image push
Plan K4 / spec §15.5 / spec §16 Phase 5.

.github/workflows/docker-image.yml builds and pushes
ghcr.io/<owner>/od on three triggers:

  - push to main          → :edge + :sha-<short> + :main-<UTC>
  - tag vX.Y.Z            → :X.Y.Z + :latest + :sha-<short>
  - pull request          → smoke build only (no push)
  - workflow_dispatch     → manual trigger

Multi-arch via QEMU + Buildx (linux/amd64 + linux/arm64).
Authenticates against GHCR via GITHUB_TOKEN with packages:write.
Uses GitHub Actions cache (type=gha) to keep rebuilds fast.

The build-args override to node:24-bookworm-slim that spec §15.1
nominates is intentionally NOT applied yet — the in-tree
deploy/Dockerfile uses alpine + apk for build tooling, and switching
the base needs the apk lines re-cast as apt-get. That's a follow-up;
the canonical alpine image is functionally equivalent for v1.

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 13:39:17 +00:00
Cursor AgentandTom Huang 6b114910e4 feat(plugins): od.genui.surfaces[].component manifest field (Phase 4)
Plan K3 / spec §10.3.5 alignment-roadmap row 2.

Plugin authors can now declare a per-surface React component path:

  od.genui.surfaces[*].component: {
    path:    string,           # required, no traversal segments
    export?: string,           # optional named export
    sandbox?: 'iframe' | 'react'
  }

The capability gate is wired in three places:

- packages/contracts: GenUISurfaceSpecSchema.component is a passthrough
  Zod object so the validator accepts the new field on round-trip.
- apps/daemon/src/plugins/trust.ts: 'genui:custom-component' joins the
  KNOWN_TOP_LEVEL_CAPABILITIES set so od plugin trust can grant it.
- apps/daemon/src/plugins/doctor.ts: doctorPlugin emits
  genui.component-capability when a surface ships a component without
  the matching capability declared, and genui.component-traversal when
  the path includes '..'.

The web GenUISurfaceRenderer (apps/web/src/components/GenUISurfaceRenderer.tsx)
keeps its built-in renderer for v1; loading the bundled component is
the next slice and depends on a sandbox wrapper that doesn't exist
yet (the spec §9.2 preview sandbox is the inspiration but
insufficient — components need React boundaries).

Daemon tests: 1490 → 1496 (+6 cases on plugins-genui-component:
schema accepts the new field shape, rejects empty path, capability
appears in the §5.3 vocabulary, doctor errors when capability is
missing, doctor passes when granted, doctor catches traversal).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 13:37:20 +00:00
Cursor AgentandTom Huang 9e12d146e2 feat(deploy): Helm chart templates (Phase 5)
Plan K2 / spec §15.5.

Lands the canonical Kubernetes shape behind the parameter surface
shipped in J4. Six templates:

  - _helpers.tpl     name / fullname / labels / selector helpers
  - secret.yaml      OD_API_TOKEN + ANTHROPIC_API_KEY + TAVILY_API_KEY
                     (operators inject through values, not committed)
  - configmap.yaml   non-secret env block from .Values.env
  - pvc.yaml         /data/od + /data/config (gated by persistence.enabled)
  - service.yaml     ClusterIP / LoadBalancer per .Values.service.type
  - deployment.yaml  pod with envFrom both maps, /api/daemon/status
                     liveness + readiness probes, persistent volume
                     mounts at OD_DATA_DIR / OD_MEDIA_CONFIG_DIR,
                     checksum/config + checksum/secret annotations
                     so a values change rolls the pod
  - ingress.yaml     optional, gated by ingress.enabled
  - NOTES.txt        post-install instructions including
                     port-forward + bearer-token reminder

The chart now installs end-to-end with:

  helm install od ./tools/pack/helm/open-design     --set secrets.apiToken=$(openssl rand -hex 32)

Per-cloud override files (values-aws.yaml, values-gcp.yaml, …) stay
scheduled — they're tiny diffs against this baseline.

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 13:32:45 +00:00
Cursor AgentandTom Huang bef9add981 feat(plugins): bound-API-token guard + bearer middleware (Phase 5)
Plan K1 / spec §15.7 / §16 Phase 5.

The daemon now ships a two-part hosted-mode safety floor:

1. startServer() refuses to start when OD_BIND_HOST is set to a
   non-loopback address and OD_API_TOKEN is unset. Loopback hosts
   (127.0.0.1 / ::1 / localhost) keep the existing zero-config
   desktop / dev experience. The error message points the operator
   at `openssl rand -hex 32` so the next attempt succeeds in one
   step.

2. When OD_API_TOKEN is set, every /api/* request must carry an
   `Authorization: Bearer <OD_API_TOKEN>` header. Three escape
   hatches:
     - Loopback peers (req.socket.remoteAddress matches
       isLoopbackPeerAddress) skip the check — the desktop UI / local
       CLI never need a bearer.
     - The /api/health, /api/version, and /api/daemon/status probes
       remain open so monitoring + cloud orchestrators (k8s readiness,
       Compose healthcheck) work without touching secrets.
     - Mismatched / missing headers return 401 API_TOKEN_REQUIRED.

This closes a previously documented Phase 5 hazard: an operator
spinning up the daemon with OD_BIND_HOST=0.0.0.0 (the docker-compose
default) without a token would have published an unsecured API.

Daemon tests: 1486 → 1490 (+4 cases on api-token-guard:
non-loopback bind without token rejected, public host with token
boots, loopback callers still pass without bearer, probes stay open).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 13:29:31 +00:00
Cursor AgentandTom Huang 735b7bdffb docs(plugins): record J-series — AG-UI adapter + atom-block + Phase 5 entry slice
CHANGELOG.md ## Unreleased gets a new bullet block summarising:
  - @open-design/agui-adapter package + GET /api/runs/:runId/agui
    (spec §10.3.5).
  - renderActiveStageBlock + loadAtomBodies (spec §23.3.2 substrate).
  - Phase 5 Dockerfile bundles plugins/_official/, plus
    tools/pack/docker-compose.yml and tools/pack/helm/open-design/
    parameter surface.

docs/plans/plugins-implementation.md §3 / §6 / §9 flipped:
  - Architecture-state table records agui-adapter, atom-bodies,
    atom-block, docker-compose, Helm values.
  - HTTP route table marks /api/runs/:runId/agui shipped.
  - §6 Phase 4 'AG-UI adapter package' deliverable flips. The
    'plugins/_official/atoms/<atom>/SKILL.md extraction' deliverable
    is recorded as substrate-shipped; the composeSystemPrompt
    rewiring stays as the next planned PR.
  - §6 Phase 5 deliverable list flips Dockerfile + docker-compose +
    Helm + snapshot retention worker; the bound-API-token guard +
    pluggable storage / Postgres adapters stay open.
  - §9 Status snapshot updated; head of the queue is now the
    composeSystemPrompt rewiring + od.genui.surfaces[].component
    upgrade + CI pushing :edge / :<version> + bound-API-token guard
    + Postgres / S3 adapters.

Final gates: pnpm guard ✓, daemon 1486 tests ✓, web 586 tests ✓,
contracts 12 tests ✓, agui-adapter 9 tests ✓.

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 13:24:53 +00:00
Cursor AgentandTom Huang bf30b308e3 feat(deploy): docker-compose + Helm chart entry slice (Phase 5)
Plan J4 / spec §15.4 / §15.5 / §16 Phase 5.

Three landings:

- deploy/Dockerfile now COPYs plugins/_official/ into the image so
  the bundled atom plugins from §3.I3 register on container boot —
  without this, registerBundledPlugins() silently no-ops inside the
  container and the §23 self-bootstrap promise breaks for hosted
  deployments.

- tools/pack/docker-compose.yml ships the canonical hosted-mode
  manifest spec §15.4 calls out: two-volume layout (od-data +
  od-config) per §15.2, OD_BIND_HOST=0.0.0.0 + OD_API_TOKEN +
  OD_NAMESPACE + snapshot retention knobs as env, /api/daemon/status
  as the healthcheck endpoint (Phase 1.5). Drop-in usable with
  `docker compose -f tools/pack/docker-compose.yml up -d`.

- tools/pack/helm/open-design/{Chart.yaml,values.yaml,README.md}
  pins the Helm chart parameter surface for the per-cloud overrides
  spec §15.5 enumerates (AWS / GCP / Azure / Aliyun / Tencent /
  Huawei / self-hosted). Templates land in the Phase 5 follow-up
  PR; the values schema is locked here so the per-cloud override
  files (values-<cloud>.yaml) review in isolation.

scripts/guard.ts allowlist gains
`packages/agui-adapter/esbuild.config.mjs` so the new package
passes the residual-JS guard.

Daemon tests stay at 1486/1486 (deploy artifacts only).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 13:20:53 +00:00
Cursor AgentandTom Huang 847304ebc5 feat(plugins): atom SKILL.md body loader + renderActiveStageBlock (spec §23.4)
Plan J3 / spec §23.3.2 patch 2 / §23.4.

Lays the substrate slice for migrating prompt fragments out of
`apps/daemon/src/prompts/system.ts` and into the bundled atom
SKILL.md bodies registered by §3.I3.

apps/daemon/src/plugins/atom-bodies.ts owns the daemon-side loader:

  loadAtomBodies(db, atomIds) → AtomBodyEntry[]

The function looks each atom id up in installed_plugins (bundled
rows win), reads the matching fsPath/SKILL.md, strips
front-matter, and returns the raw body. Atoms with no installed
plugin or unreadable SKILL.md are silently skipped — the caller
drops empty entries from the prompt.

packages/contracts/src/prompts/atom-block.ts ships the pure
renderer:

  renderActiveStageBlock({ stageId, bodies, iteration? }) → string

Mirrors spec §23.4's composeSystemPrompt sketch. Empty bodies
return ''; multiple bodies are separated by '---' with no trailing
separator. Lives in contracts so the daemon-side composer and any
future contracts-side composer share one definition (§11.8 PB1
single-import guarantee).

The composeSystemPrompt() rewiring itself is the next PR — this
commit gives that PR zero scaffolding to build: the helpers are
reachable, tested, and the bundled atom plugins from §3.I3 already
have the matching SKILL.md bodies on disk.

Tests: contracts 8 → 12 (+4 cases on atom-block); daemon
1482 → 1486 (+4 cases on plugins-atom-bodies covering the
end-to-end loadAtomBodies → renderActiveStageBlock path).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 13:15:52 +00:00
Cursor AgentandTom Huang 0631f04a00 feat(plugins): @open-design/agui-adapter package + GET /api/runs/:id/agui
Plan J1 + J2 / spec §10.3.5 / Phase 4.

New workspace package: packages/agui-adapter/. Pure-TS
bidirectional bridge between OD's native PersistedAgentEvent /
GenUIEvent / PluginPipelineStageEvent union and the AG-UI canonical
event protocol (https://github.com/CopilotKit/CopilotKit).

  - src/types.ts        — AGUIEvent discriminated union (agent.message,
                          tool_call, state_update, ui.surface_requested,
                          ui.surface_responded, run.lifecycle).
  - src/encode.ts       — encodeOdEventForAgui(event, ctx): maps every
                          OD native event onto the canonical shape; drops
                          events the encoder can't translate so external
                          AG-UI clients always see a clean stream.
  - tests/encode.test.ts (9 cases) covers message_chunk, tool_call,
                          run_started, end → started/completed/failed/
                          cancelled, pipeline_stage_started/completed,
                          genui_surface_request/response/timeout,
                          genui_state_synced, and the unknown-event drop.

apps/daemon/src/server.ts mounts GET /api/runs/:id/agui:

  - 404 for unknown run ids.
  - Replays the run's recorded events through the encoder on subscribe
    (so a reconnecting client with Last-Event-ID picks up exactly the
    AG-UI events it missed).
  - Subscribes to future events via a thin adapter client wrapper that
    routes through the existing run.clients fan-out, so the encoder
    runs lazily on each broadcast (no double event buffering).

Daemon depends on @open-design/agui-adapter; the package builds clean
and ships pure ESM. v1 plugins consume CopilotKit / agent-protocol
clients without modification — the adapter ships independently from
daemon main, so upstream protocol revs do not couple to the daemon
release cadence (per spec §10.3.5 Phase 4 contract).

Tests: agui-adapter 9/9, daemon 1481 → 1482 (+1 case on agui-route).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 13:11:48 +00:00
Cursor AgentandTom Huang d06c1b4905 docs(plugins): record I-series — pipeline-into-startChatRun + doctor/config + bundled atoms
CHANGELOG.md ## Unreleased gets a new bullet block summarising:
  - Pipeline runner wired into POST /api/runs (e2e-3 full §8 contract).
  - od doctor (repo-wide diagnostics) + od config get/set/list/unset.
  - Phase 4 §23 entry slice — bundled atom plugins + boot walker.

docs/plans/plugins-implementation.md §3 / §6 / §8 / §9 flipped:
  - Architecture-state table records scaffold / export / publish /
    bundled / plugins/_official/atoms as shipped.
  - §6 Phase 4 deliverable list flips bundled atom extraction +
    full CLI parity remainder; the system.ts → SKILL.md prompt
    rewiring stays open as the next planned PR.
  - §8 e2e-3 row flips from entry-slice to full §8 contract:
    plugins-headless-run.test.ts asserts the first SSE event on a
    pipeline-bearing run is pipeline_stage_started.
  - §9 Status snapshot updated; head of the queue is now
    composeSystemPrompt() reading atom fragments from
    plugins/_official/atoms/<atom>/SKILL.md, then the AG-UI adapter,
    then Phase 5 Docker image.

Final gates: pnpm guard ✓, daemon 1481 tests ✓, web 586 tests ✓.

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 13:03:57 +00:00
Cursor AgentandTom Huang 2848327199 feat(plugins): bundled atom plugins + boot walker (Phase 4 / spec §23)
Plan I3 / spec §23.3.5 + §23.3.4.

Lays the entry slice for spec §23 self-bootstrapping: the daemon's
own behavioural content moves out of system.ts hard-coded constants
and into per-atom SKILL.md files under plugins/_official/atoms/.

New ground:

- plugins/_official/atoms/ ships four canonical atom SKILL.md +
  open-design.json pairs (discovery-question-form, todo-write,
  direction-picker, critique-theater). Each pair is a fully valid
  plugin that the existing local-folder installer can pick up; the
  manifest declares od.kind='atom' so the future doctor warns when a
  pipeline references the matching id but the bundled folder is
  missing.

- apps/daemon/src/plugins/bundled.ts owns the boot walker:
  registerBundledPlugins({ db, bundledRoot }) walks one level of
  subdirectories (atoms/, scenarios/, bundles/) plus direct
  plugins, registers every match into installed_plugins under
  source_kind='bundled' / trust='bundled', and is idempotent across
  daemon restarts (upserts on every boot).

- defaultBundledRoot(workspaceRoot) returns
  <workspaceRoot>/plugins/_official; server.ts boot calls
  registerBundledPlugins(..., defaultBundledRoot(PROJECT_ROOT)) and
  prints a one-line summary. ENOENT is silent — running the daemon
  outside the dev tree just skips this step.

The full §23 lift (system.ts prompt fragments → SKILL.md bodies +
plugin.kind='atom' → composeSystemPrompt resolving each stage's
atoms[] through the registry) stays open. This commit is the
substrate slice: bundled plugins are now first-class
installed_plugins rows under the new tier, doctor / list / info /
apply all pick them up, and a future PR that wires composeSystemPrompt
to read SKILL.md bodies has zero registration work to do.

Daemon tests: 1476 → 1481 (+5 cases on plugins-bundled covering
tiered + direct layouts, idempotent reruns, ENOENT, miss-skips).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 13:00:27 +00:00