mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
fbddf11f8bd1ccdb45dc09653b6f3459f18ab256
359
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
729ef2ebb5 |
feat(plugins): od plugin validate <folder> author-side lint (Phase 4)
Plan W1 / spec §11.5 / §16 Phase 4.
apps/daemon/src/plugins/validate.ts ships the pre-install lint
helper:
validatePluginFolder({ folder, registry?, connectorProbe? })
\u2192 { ok, resolveErrors, resolveWarnings, doctor?, folder }
Reads the folder via the same resolvePluginFolder() the installer
uses (so manifest parsing is byte-equal), then runs doctorPlugin()
against the supplied registry view. Without a registry, ref-bound
checks skip cleanly (offline mode); with an empty/full registry,
unresolved skill / DS / atom refs surface as warnings.
flattenValidationDiagnostics(result) \u2192 Diagnostic[]
Convenience flattener for renderers that don't want to special-case
resolve vs. doctor diagnostics.
apps/daemon/src/cli.ts `od plugin validate <folder>`:
Usage: od plugin validate <folder> [--json] [--no-daemon] [--daemon-url <url>]
Optional --no-daemon skips the registry fetch (works fully
offline). When the daemon is reachable, the registry view is
fetched from /api/skills + /api/design-systems + /api/atoms so
ref checks happen against the operator's actual library.
Exit codes:
0 doctor.ok = true
4 doctor.ok = false (errors present)
2 CLI usage error / folder unreadable
Help block in printPluginHelp() updated.
Daemon tests: 1666 \u2192 1674 (+8 cases on plugins-validate: clean
manifest passes, empty folder errors at resolve, malformed JSON
parse error, unknown atom id error, unparseable until expression
error, missing skill ref no-op without registry, missing skill ref
flagged with empty registry, flatten ordering).
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
671293e7f4 |
feat(cli): od doctor + od config get/set/list/unset
Plan I2 / spec §12.2. `od doctor` is a single-shot diagnostics command that: - hits /api/daemon/status (fails with exit 64 if unreachable) - lists installed plugins and runs the per-plugin doctor route - lists skills / design-systems / atoms for inventory - aggregates issues by severity (warn / error) - exits 1 when any plugin doctor returns ok=false, otherwise 0 This is the CLI entry point a CI runner uses to gate a release on plugin-system health before publishing. --json emits the full report so downstream tooling (e.g. a GitHub Action summary step) can render its own table. `od config list/get/set/unset` wraps GET/PUT /api/app-config so a code agent can flip provider keys / orbit settings / pet config headlessly: od config list od config get <key> (or --json for the value alone) od config set <key> <value> (positional or --value <text>) od config set <key> --value-json '<json>' (nested values) od config unset <key> set on a top-level key only — nested merges go through --value-json which keeps the daemon's existing PUT semantics (full-document replace) intact. Daemon test suite stays at 1476/1476 (the new CLI verbs are thin wrappers over endpoints that already have integration coverage). Co-authored-by: Tom Huang <1043269994@qq.com> |
||
|
|
deb74d91c4 |
feat(plugins): wire pipeline runner into POST /api/runs (e2e-3 full contract)
Plan I1 + I4 / spec §10.1 / plan §8 e2e-3. Lifts e2e-3 from entry-slice to the full §8 contract: 'first ND-JSON event has kind=pipeline_stage_started'. POST /api/runs gains a synchronous firePipelineForRun() call that runs immediately after design.runs.create() + linkSnapshotToRun(), BEFORE design.runs.start() schedules the agent. The first pipeline_stage_started event lands on the run's SSE buffer ahead of any message_chunk the agent emits. The remaining stage timeline (stage_started/completed for stages 2..N, plus the run_devloop_iterations audit row per stage) fires asynchronously while the agent runs. The stage runner is a stub that returns converged signals (critique.score=4, preview.ok=true, user.confirmed=true) so a non-loop pipeline walks through every stage in O(stages) time. Loop stages still respect OD_MAX_DEVLOOP_ITERATIONS. Phase 4's atom migration into plugins/_official/atoms/<atom>/SKILL.md will swap the stub for a real per-stage worker that drives the agent. Errors inside the pipeline runner emit pipeline_stage_failed onto the run stream and never block the agent process — a buggy plugin manifest can never deadlock a chat run. apps/daemon/tests/plugins-headless-run.test.ts gains a second case that installs a fixture plugin with two declared stages, creates a project + run, then reads the SSE event stream and asserts the first observed stage event is pipeline_stage_started. The fixture grants pipeline:* via the resolver's grantCaps[] field so the restricted-trust capability gate stays in force for non-pipeline runs. Daemon tests: 1475 → 1476. Co-authored-by: Tom Huang <1043269994@qq.com> |
||
|
|
712c18dbc2 |
feat(cli): od atoms / skills / design-systems / craft / status / version + marketplace search
Plan H2 + H3 + H4 / spec §12.2 / §11.7.
Closes the CLI parity remainder: every UI feature reachable via
/api/* now has a matching CLI verb.
od atoms list List first-party atoms.
od atoms show <id> Print one atom (status + taskKinds).
od skills list / show <id> Wraps GET /api/skills{,/:id}.
od design-systems list / show <id> Wraps GET /api/design-systems{,/:id}.
od craft list / show <id> NEW endpoints GET /api/craft{,/:id}
walk the craft/ directory and
emit { id, label, bytes }.
od status Alias of `od daemon status`.
od version Wraps GET /api/version; --json
emits the structured shape.
od marketplace search "<query>" New verb. Walks every configured
[--tag <tag>] marketplace's plugins[] and
matches by substring on
name + description + tags.
The HTTP additions are GET /api/craft (catalog summary) and
GET /api/craft/:id (raw markdown body), both safe-slug-validated.
Daemon tests: 1472 → 1475 (+3 cases on craft-route covering catalog
shape, malformed slug rejection, miss-as-404).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
45f52d71c4 |
feat(plugins): od plugin publish — catalog PR-template launcher
Plan H1 / spec §14.1.
apps/daemon/src/plugins/publish.ts owns the URL-builder side. Pure
string assembly per catalog target:
anthropics-skills → github.com/anthropics/skills/issues/new
?title=…&body=…
awesome-agent-skills → github.com/VoltAgent/awesome-agent-skills/...
clawhub → github.com/openclaw/clawhub/...
skills-sh → https://skills.sh/ + npx skills add owner/repo
(skills.sh autodiscovers; no PR form needed)
The PR body carries plugin id / version / repo URL plus a compatibility
checklist confirming SKILL.md + open-design.json so the upstream
reviewer can validate against the §3 cross-catalog matrix at a glance.
CLI:
od plugin publish <pluginId> --to <catalog>
[--repo <github-url>] [--snapshot-id <id>]
[--open] [--json]
--open auto-launches the system browser (open / xdg-open / start) on
the catalog URL, so a code agent driving the headless flow can hand
off to the human author with a single command.
The CLI never POSTs anywhere — every catalog upstream stays under the
author's review control.
Daemon tests: 1465 → 1472 (+7 cases on plugins-publish covering URL
shape per target + skills.sh fallback + unknown-catalog rejection).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
411d83b0bf |
feat(web): MarketplaceView + PluginDetailView + /marketplace routes
Plan G4 / spec §11.6.
router.ts gains two new Route variants — 'marketplace' and
'marketplace-detail' — plus parsing for both /marketplace/<id> and
the /plugins/<id> alias the public site (§13) reserves. App.tsx
dispatches them outside the EntryView / ProjectView split so the
discovery surface stays independent of any active project.
New components:
- MarketplaceView (apps/web/src/components/MarketplaceView.tsx)
- Card grid of every installed plugin with trust-tier filters
(All / Trusted / Restricted).
- Secondary 'Configured catalogs' panel listing every row in
/api/marketplaces with id / url / trust / plugin count.
- Cards link to /marketplace/<id>.
- PluginDetailView (apps/web/src/components/PluginDetailView.tsx)
- Loads /api/plugins/:id, renders header (title, version, trust,
sourceKind, taskKind), description, capability checklist,
connector requirements (required + optional), and declared GenUI
surfaces.
- 'Use this plugin' button calls applyPlugin(id) and navigates
back to Home so the existing inline rail / NewProjectPanel
surface picks up the snapshot.
Web tests: 579 → 586 (added router-marketplace 5 cases +
MarketplaceView 2 cases). Typecheck clean.
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
b436f0b33d |
feat(web): mount PluginsSection in ChatComposer
Plan G3 / spec §8.4 / §11.6.
ChatComposer renders `<PluginsSection variant="strip" />` directly
above the existing `composer-input-wrap` whenever a projectId is
known. Behaviour mirrors the NewProjectPanel mount from §3.F5:
- The strip variant is the slim horizontal layout the spec calls
out for ChatComposer (versus 'wide' on Home).
- Apply hydrates the draft (only when empty), so a user mid-typing
is never overwritten.
- The existing send / @-mention / staged-attachment chrome is
untouched.
The deeper inputs-form gating + chip-strip removal of the active
plugin go through the same PluginsSection state, so a Phase 2B
follow-up that wires the Send-button to onValidityChange will not
need to touch ChatComposer again.
Web tests stay at 579/579 (the strip variant flows through the same
component primitives that already have jsdom coverage).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
04784013e6 |
feat(plugins): od plugin scaffold + od plugin export (Phase 4 author tooling)
Plan G1 + G2 / spec §14.
Two new author-side CLI verbs that close the spec §14 'one repo, every
catalog' loop without leaving the terminal.
apps/daemon/src/plugins/scaffold.ts owns the scaffold path:
od plugin scaffold --id <id> [--title] [--description]
[--task-kind <new-generation|code-migration|
figma-migration|tune-collab>]
[--mode <m>] [--scenario <s>]
[--out <dir>] [--with-claude-plugin]
Writes <out>/<id>/{SKILL.md, open-design.json, README.md} (plus the
optional .claude-plugin/plugin.json for cross-catalog publishing).
Refuses to overwrite an existing scaffold; rejects unsafe ids.
apps/daemon/src/plugins/export.ts + POST /api/applied-plugins/export
own the export path. Three targets:
od → SKILL.md + open-design.json + provenance README
claude-plugin → SKILL.md + .claude-plugin/plugin.json + README
agent-skill → SKILL.md + README only (every catalog accepts this)
od plugin export <projectId> --as od|claude-plugin|agent-skill --out <dir>
od plugin export --snapshot-id <id> --as <target> --out <dir>
The exporter pulls SKILL.md straight off the installed plugin's fsPath
when available and otherwise synthesises one from the snapshot's
frozen plugin metadata, so a publishable folder is reproducible even
after od plugin update / uninstall rotates the live source. The
generated open-design.json carries a provenance block recording
snapshot id + manifestSourceDigest so a downstream catalog viewer can
reverse-resolve the originating run.
The HTTP route is loopback-only (requireLocalDaemonRequest) because
it writes to the host filesystem; the CLI is the canonical caller.
Daemon tests: 1455 → 1465 (+10): plugins-scaffold (5 cases) +
plugins-export (5 cases).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
daceadefbd |
feat(web): mount PluginsSection in NewProjectPanel
Plan §3.F5 / spec §8 / §11.6.
New apps/web/src/components/PluginsSection.tsx bundles InlinePluginsRail,
ContextChipStrip, and PluginInputsForm into one host-agnostic widget.
The host wires three optional callbacks:
- onApplied(brief, applied) fires on apply + every input change
- onCleared() fires when the user removes a context
chip (clears the active plugin)
- onValidityChange(valid) mirrors the inputs-form validity gate
renderPluginBriefTemplate substitutes {{var}} placeholders inside
useCase.query against the live inputs map so the brief output stays
in sync as the user types.
NewProjectPanel mounts PluginsSection right under the project-name
input. The section is purely additive: clicking a plugin card hydrates
the project name field (only when empty) with the rendered brief, so
the existing Send-button rules are unchanged. The deeper composer
gating + ChatComposer mount stay scheduled for the Phase 2B PR.
Web test suite: 575 → 579 (added 4 cases on PluginsSection: empty
state, apply hydration, input change re-emission, chip-remove
clearing).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
921df7cbbd |
test(plugins): e2e-3 headless install→project→run anchor
Plan §3.F4 / spec §8 e2e-3 (entry slice).
apps/daemon/tests/plugins-headless-run.test.ts walks the §12.5
headless flow at the HTTP layer with no electron / no agent backend:
1. POST /api/plugins/install (local fixture, SSE) succeeds.
2. POST /api/projects { pluginId, pluginInputs } resolves via the
snapshot resolver and returns appliedPluginSnapshotId.
3. POST /api/runs { projectId, pluginId, appliedPluginSnapshotId,
pluginInputs } returns 202 + runId.
4. GET /api/runs/:id surfaces the same appliedPluginSnapshotId on
the run status payload (the JSON shape od run watch / od run info
read against).
5. GET /api/applied-plugins/:id returns the same snapshot the
replay path reads.
The full e2e-3 contract from §8 ('first ND-JSON event has
kind=pipeline_stage_started') still depends on plugging
runPipelineForRun() into startChatRun()'s actual agent loop — that
slice is deferred (and called out in plan §3.A7). This test locks
the public HTTP shape so the next slice doesn't accidentally drift
the snapshot wiring while wiring the agent loop.
Daemon tests: 1454 → 1455.
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
9927707361 |
feat(plugins): od plugin install <name> resolves through marketplaces
Plan §3.F3 / spec §7.2 + §6 / §16 Phase 3. `resolvePluginInMarketplaces(db, name)` walks every configured marketplace in registration order and returns the first matching plugin entry. The matched entry carries: - the canonical source string (github:owner/repo, https://…tar.gz) that the existing installer backends already understand - the marketplace trust tier and id for the audit trail - the optional description for surface display POST /api/plugins/install detects bare-name sources (anything that isn't a local path / github: / https://) and re-routes through `resolvePluginInMarketplaces` before handing off to the installer. On miss the route returns 404 plugin-not-found with structured error data, so the CLI's structuredHttpFailure helper maps it to exit 65. `od plugin install <name>` (no flag) now works end-to-end. The existing `./folder`, `github:…` and `https://*.tar.gz` shapes are unchanged. Note: per spec §9, marketplace trust does NOT propagate to the installed plugin: a plugin pulled from a 'trusted' marketplace still defaults to its source-kind tier. Trust elevation goes through `od plugin trust` / the Phase 3 trust UI. Daemon tests: 1450 → 1454 (added 4 cases to plugins-marketplaces: canonical-source resolution, case insensitivity, miss → null, registration-order tie-break). Co-authored-by: Tom Huang <1043269994@qq.com> |
||
|
|
1ae977e066 |
feat(cli): od daemon start/status/stop + new HTTP daemon-lifecycle routes
Plan §6 Phase 1.5 / spec §11.7.
The desktop default `od` (no subcommand) keeps its current behaviour
for back-compat. New explicit lifecycle entry:
od daemon start [--headless] [--serve-web] [--port <n>] [--host <addr>]
[--no-open] [--namespace <ns>]
--headless / --no-open / --serve-web all imply 'do not auto-open the
browser', so a code agent / CI runner / Docker entry point can launch
the daemon without electron. The port + host flags fall through to
OD_PORT / OD_BIND_HOST when unset, matching every other surface.
od daemon status [--json] [--daemon-url <url>]
od daemon stop [--daemon-url <url>]
These two CLI verbs hit the new HTTP routes:
GET /api/daemon/status → { ok, version, bindHost, port, pid,
dataDir, mediaConfigDir, namespace,
installedPlugins, shuttingDown }
POST /api/daemon/shutdown → 202; emits SIGTERM so the existing
graceful close path runs unchanged.
requireLocalDaemonRequest enforces the
same loopback / origin check used by
composio config writes (no public
shutdown surface).
This makes `od daemon start --headless --port 17456` followed by
`od plugin install ./fixtures/sample-plugin` and the rest of the
§12.5 walkthrough reachable without electron — exactly the substrate
e2e-3 needs.
Daemon tests: 1448 → 1450 (added daemon-lifecycle with 2 cases:
status snapshot shape; shutdown rejects non-loopback callers).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
3d0ce28346 |
feat(cli): od project / run / files / conversation subcommand routers
Plan §6 Phase 1 follow-up + Phase 2C / spec §12.2.
Wraps the existing daemon HTTP endpoints behind stable CLI subcommands
so a code agent can drive the §12.5 walkthrough end-to-end without
hitting /api/* directly. Every UI feature now has a CLI equivalent
(spec §11.7 invariant).
New CLI groups:
od project create [--name] [--skill] [--design-system] [--plugin]
[--inputs <json>] [--metadata-json <path|->]
[--grant-caps a,b]
od project list / info / delete
od run start --project <id> [--conversation] [--message] [--plugin]
[--inputs <json>] [--grant-caps] [--agent] [--model]
[--snapshot-id] [--follow]
od run watch <runId> ND-JSON SSE relay on stdout
od run cancel <runId>
od run list [--project <id>]
od run info <runId>
od files list <projectId>
od files read <projectId> <relpath>
od files write <projectId> <relpath> (reads stdin)
od files upload <projectId> <localpath> [--as <relpath>]
od files delete <projectId> <name>
od conversation list <projectId>
od conversation info <conversationId>
Plugin runs route through the snapshot resolver from §3.A1 — a code
agent can pass --plugin / --inputs / --grant-caps directly to
`od project create` or `od run start` and the daemon pins the
applied plugin snapshot to the new project / run. Capability gate
failures map to exit 66 / capabilities-required, missing inputs to
exit 67 / missing-input via the structured error envelope from §3.B1.
Daemon test suite stays at 1448/1448 (CLI handlers are thin HTTP
wrappers; the underlying routes already have integration coverage).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
adc2afd769 |
feat(web): plugin composer surface — applyPlugin + Rail + Inputs + GenUI renderer
Plan §3.C1–§3.C4.
Web composer integration for the plugin system:
- apps/web/src/state/projects.ts gains:
* applyPlugin(pluginId, { inputs?, projectId?, grantCaps? }) — wraps
POST /api/plugins/:id/apply and returns the typed ApplyResult.
* listPlugins() — wraps GET /api/plugins.
* renderPluginBriefTemplate(template, inputs) — substitutes
{{var}} placeholders inside useCase.query as the user types so the
composer's brief textarea re-renders live.
- New components:
* InlinePluginsRail — the card strip that lives below the input box
on Home and inside ChatComposer. Supports 'wide' / 'strip' layouts
+ taskKind / mode filters.
* ContextChipStrip — typed ContextItem chips above the brief input.
Optional onRemove for clearing the applied plugin.
* PluginInputsForm — JSON-Schema-light form rendered between the
input and Send. Required fields gate Send via onValidityChange;
string/text/select/number/boolean field types are supported.
* GenUISurfaceRenderer — first-class confirmation + oauth-prompt
surfaces (form + choice fall back to a JSON Schema preview +
free-form textarea until Phase 2A.5).
* GenUIInbox — drawer that lists every persisted surface answer for
a project; revoke calls POST /api/projects/:id/genui/:sid/revoke.
- jsdom tests under apps/web/tests/components/:
* InlinePluginsRail (mount fetch, click → applyPlugin → onApplied,
taskKind filter)
* PluginInputsForm (validity gating, default hydration, select
options)
* GenUISurfaceRenderer (confirmation true/false branches; oauth
surface forwards { authorized, connectorId } per spec §10.3.1)
Web test suite: 567 → 575 (added 8 plugin component cases). The
NewProjectPanel / ChatComposer / ProjectView mounts will land in the
follow-up commit so this PR's diff stays reviewable.
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
5961c877b6 |
test(plugins): definition-of-done suite — e2e-2, e2e-4, e2e-6, e2e-8
Plan §8.
apps/daemon/tests/plugins-dod-e2e.test.ts anchors the four §8 e2es that
don't require Playwright UI flow or the Phase 1.5 headless flag:
e2e-2 pure apply across runs: two consecutive applies share digest
and produce no FS mutation. Snapshot count stays at 0 because
applyPlugin() itself never writes to applied_plugin_snapshots.
e2e-4 replay invariance: the snapshot's prompt block (rendered via
renderPluginBlock from @open-design/contracts) is byte-equal
before and after a same-id plugin upgrade, even though the
live applyPlugin against the upgraded plugin produces a
different manifestSourceDigest.
e2e-6 connector trust gate: a restricted plugin that declares
od.connectors.required=[{id:'slack'}] without connector:slack
capability fails resolvePluginSnapshot with HTTP 409 / exit 66
and capabilitiesRequiredError envelope shape. Independently the
token-issuance gate (checkConnectorAccess) refuses the same
call, so even a leaked tool token can't bypass §5.3.
e2e-8 apply purity regression: 100 applies leave the project cwd
byte size unchanged, write zero .mcp.json files, and grow the
snapshot count by exactly 100 only when the resolver path
explicitly persists.
Plus a guardrail on capabilitiesRequiredError() so the §12.4 / exit-66
data shape stays stable for code agents reading it.
Daemon tests: 1443 → 1448 (+5).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
0df9176c2f |
feat(plugins): marketplace registry + od marketplace CLI verbs
Plan §3.B4 / spec §6 / §7.2 / §16 Phase 3 entry slice.
The storage half of federated catalogs:
- New apps/daemon/src/plugins/marketplaces.ts owns add / list / get /
refresh / remove / set-trust over the pre-existing plugin_marketplaces
SQLite table. Uses parseMarketplace from @open-design/plugin-runtime so
validation matches the schema in docs/schemas/open-design.marketplace.v1.json.
Default trust tier is 'restricted' (discovery only) per spec §9.
- HTTP routes:
* GET /api/marketplaces
* POST /api/marketplaces (body { url, trust? })
* GET /api/marketplaces/:id
* DELETE /api/marketplaces/:id
* POST /api/marketplaces/:id/refresh
* POST /api/marketplaces/:id/trust (body { trust })
* GET /api/marketplaces/:id/plugins (catalog passthrough)
- CLI verbs: `od marketplace add/list/info/refresh/remove/trust`.
--trust accepts 'trusted', 'restricted', or 'official'.
Phase 3 follow-up wires `od plugin install <name>` resolution + the trust
UI on top; this commit is the data-layer half so the CLI can already
register and inspect catalogs.
Daemon tests: 1438 → 1443 (added plugins-marketplaces with 5 cases).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
fbabb27143 |
feat(plugins): CLI structured error envelope + apply --grant-caps / --input k=v
Plan §3.B1 + §3.B2 / spec §12.4.
- New exitWithStructuredError() + structuredHttpFailure() helpers in
cli.ts. Maps a daemon HTTP error envelope (or a synthetic local
reason) to the §12.4 exit codes (64–73): daemon-not-running,
plugin-not-found, capabilities-required, missing-input,
project-not-found, run-not-found, provider-not-configured,
plugin-requires-daemon, snapshot-stale, genui-surface-awaiting.
- od plugin apply now:
* accepts --inputs <json> AND repeated --input k=v (auto-coercing
'true'/'false' booleans and numerics) so a code agent can build the
inputs map without shell-escape gymnastics
* accepts --grant-caps a,b for per-snapshot capability grants
* routes 422 (missing inputs) → exit 67, 409 capabilities-required →
exit 66, daemon-down → exit 64, all via the structured envelope.
apps/daemon/src/plugins/installer.ts gets a small TS soundness fix on
the tar filter callback (entry can be Stats or ReadEntry — narrow before
reading .type).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
531df0e270 |
feat(plugins): pipeline-runner — stage events + GenUI cache integration
Plan §3.A7 + §3.A8 / spec §10.1 / §10.2 / §10.3.3.
New apps/daemon/src/plugins/pipeline-runner.ts is the wiring layer
between the pure pipeline scheduler and a live run's event stream:
- Walks the pipeline through runPipeline(), emitting
pipeline_stage_started / pipeline_stage_completed events for SSE
consumers and persisting one row per iteration into
run_devloop_iterations.
- Pre-stage GenUI: surfaces with no trigger.stageId (e.g. the
__auto_connector_<id> oauth-prompts the apply path auto-derives) are
raised once up-front via requestOrReuseSurface(). On a project-tier
cache hit the registry emits genui_surface_response { respondedBy: 'cache' }
WITHOUT broadcasting a new request — this is the e2e-5 anchor.
- Stage-bound surfaces fire on stage entry only when the surface's
trigger.stageId matches.
The actual stage worker is supplied by the caller; in v1 the daemon
plugs in a stub that records iteration counts. Phase 4 swaps in
critique-theater scoring + build-test signals without touching this
public contract.
Daemon tests: 1434 → 1438 (added plugins-pipeline-runner with 4 cases:
event sequence, devloop convergence, e2e-5 cross-conversation cache,
stage-bound surface gating).
Co-authored-by: Tom Huang <1043269994@qq.com>
|