mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
e026a54fd933ed2b965c50b07a4c91f506735e9b
517
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|