SuperAPI AI Directory

aigo-template-transfer

Data & analytics

View repository

Updated 10-16 04:58

About this skill

AI GO Custom App → Template conversion Skill

This Skill helps an AI Agent turn a custom app into a template ready to be listed. Supports Claude Code / Antigravity / Cursor.

Phase -1: Skill self-update check (executed on every trigger)

If a SessionStart hook is installed (see the README "Staying updated"; templates exist for both Claude Code and Codex), this phase is skipped automatically (throttled) and does not need to be run again.

python scripts/check_update.py     # macOS / Linux 用 python3
  • No output = nothing to do: if it is already up to date, offline, or has been checked within the last 24 hours, it ends silently and goes straight to Phase 0.
  • Output = there is a new version: inform the user of the version gap and a summary of the changes,ask whether to update; if agreed → run the update command printed by the script, and after it completesre-read SKILL.md and the relevant references, so the new rules take effect in this very turn (the old version is already in context; not re-reading means the old doctrine continues to be used).
  • Never overwrite automatically: the user may have modified local files; do not run the update command before obtaining consent.
  • When a machine-readable result is needed, use --json(always output,status is skipped/unknown/current/outdated)。

Iron rules (read first)

  1. AI only makes judgments and proposals; scripts make the changes. All changes to template content must go through apply_decisions.py applied according to decisions.json — you must not directly edit files under work/<slug>/template/ . The only two exceptions: Phase 5 drafting actions/seed_demo_data.py(a new file, still requires user gate confirmation), and, after Phase 4, normalize_meta.py generating _template_meta.json。
  2. Human gates must not be filled in on the user's behalf. Records of decided_by: "user" in decisions.json can only be written after explicit user confirmation; the interactive confirmations of transfer_cli.py gate / confirm-source / confirm-meta and devportal.py submit(and adopt) must be entered by the user personally. AI proposals must always first be presented to the user as decided_by: "proposed" . There are six human gates in total: S0 candidate determination,source identity before S1, S3 item-by-item rulings, S5 demo data, meta storefront copy before S6, and S9 submission for review.
  3. Phases must not be skipped. The state machine (S0→S9) is enforced by scripts; the content hash gate blocks any change outside a gate.
  4. Always the new system. Only produce data_center_schema(version=1); the old-system custom_objects_schema is not read, not converted, and not output. Any old-system API encountered (ctx.db.*_object) must be rewritten to the new system.
  5. Tier C is not handled. Non-custom apps (standalone Next.js/Express/Flutter, etc.) do not enter this flow; converting them amounts to a rewrite, so explain the exclusion directly to the user.
  6. All outbound calls must go through the egress gateway ctx.http.call; keys are carried by the action itself. The correct solution for an action calling a third-party API is ctx.http.call("<egress-slug>", "<path>", method=..., body=..., headers=...), and declare that slug in the _template_meta.json of required_egress (normalize_meta.py adds it automatically). Bringing your own credentials is the right way (reversed on 2026-08-03, ADR 0010 domain-only): the gateway only verifies domains and policy, it neither injects nor strips Authorization——AI GO connector_proxy._sanitize_headers and Developer platform dev_ctx._STRIPPED Currently only strips hop-by-hop (host/content-length/ proxy-*). The key goes through setup_schema inclusion, action side ctx.secrets.get(...) reads it out to assemble headers; EgressService only provides base_url and the whitelist,auth_type uniformly none (the sandbox side filling in other values will 400). The old doctrine (before v0.6.x) said "credentials cannot be self-provided, keys are injected by EgressService" — that is already wrong — templates produced by the old approach pass sandbox testing but get 401 after going live, and the error surfaces at "tenant adds channel", the place farthest from deployment. If you see an old template using the injection style, change it to self-provided. import httpx can't get out: the runner pod is default-deny egress (ADR-0003, the SG only allows ctx-only service), raw httpx/requests will directly timeout, and sandbox testing won't pass.
  7. Credential discipline. The password exists only in ~/.aigo-transfer/.env and isthe user themselvesfilled in; the agent does not fill it in on their behalf, does not ask for the password in conversation, and does not put the password into the command line. If the user pastes the password in the conversation, remind them to fill it in .env and change it.
  8. When something goes wrong, check the table first. For any failure, first read the raw error message and then check references/troubleshooting.md, do not guess at fixes on your own; permission and configuration problems cannot be fixed by changing code.

Path conventions

In this document, the .env and work/<slug>/ mentioned are both under the user data directory ~/.aigo-transfer/ (overridable with AIGO_TRANSFER_HOME ),not inside the skill directory— when updating via copy-style installation, the skill directory is completely cleared and redeployed. Script output always prints absolute paths; when giving the user a path, copy the script output verbatim, do not assemble it yourself with <skill>/work/.... Data left in the skill directory by older versions will be automatically migrated on first execution.

Phase 0: Preflight checks (run at the start of every work session)

python scripts/devportal.py whoami      # 目的地側:Developer 平台 PAT 與權限
python scripts/aigo_client.py whoami    # 來源側:AI GO 帳號與 builder.access(抽線上 app 才需要)
  • Failure or no PAT → python scripts/devportal.py setup Guide the user according to the instructions:
    1. Log in/register at https://developer.ai-go.app(未登入先引導註冊)
    2. settings page https://developer.ai-go.app/settings → "API Token (PAT)" → issue (shown only once)
    3. python scripts/devportal.py set-pat paste in
  • level=read_only → tell the user they need to ask the platform admin to upgrade them to editor,stop here, do not attempt to bypass.
  • Source-side AI GO account (builder.access): ask the userthemselvesat ~/.aigo-transfer/.env fill in AIGO_TENANT(the first segment in the address bar at login, e.g. urfit) and AIGO_EMAIL / AIGO_PASSWORD (or AIGO_TOKEN)。logging in via tenant space https://{tenant}.ai-go.app——apex (https://ai-go.app) can no longer log in and will return a 401 with exactly the same shape as a wrong passwordexactly the same shape(the platform's anti-account-enumeration design), so the script directly blocks apex and prints the rule; when encountering a 401, both directions of "wrong password/wrong tenant" must be checked, do not just tell the user to check the password.aigo_client.get_token() will go through "token cache → refresh reissue → password login", normally seamless throughout; when a RuntimeError is thrown, pass the message to the user as-is (it contains configuration guidance). Resolve credential problems here— otherwise it will blow up halfway through S1 extraction, and the error mixed into the extraction flow is harder to diagnose. Missing builder.access is a permission configuration problem; ask the tenant admin to grant it, do not change code to work around it (only conversions from pure repo sources do not need source-side credentials, so this one can be skipped).

Phase 0.5: Candidate determination (S0, manual gate)

First take inventory, then let the user decide:

  1. Review the source app's features and scale (number of actions, number of pages, custom tables).
  2. Check whether it overlaps with existing templates (the on-shelf list is the sole authority):
python scripts/devportal.py live-templates [--query <關鍵字>]

If it is judged as "merge into existing" and thatis unmanaged(can_adopt), you must take it over first before you can modify it on this platform: python scripts/devportal.py adopt --template-slug <架上 slug>(admin、irreversible, and carries a manual confirmation gate), after taking over use pull to retrieve the content as the maintenance baseline.

  1. Report one of three recommendations to the user (open new / merge into existing / exclude) along with the reasons, and have the user execute:
python scripts/transfer_cli.py init --slug <slug>
python scripts/transfer_cli.py gate --slug <slug> --stage S0 --decision new --notes "<理由>"

slug rules:^[a-z0-9][a-z0-9_-]*$; colliding with an AI GO on-shelf slug will produce a 409 in S7. category whitelist: starter/messaging/crm/catering/integration/ai/operations/productivity/analytics. access_mode cannot be changed after creation, so think it through first.

Phase 1: Extraction and normalization (S1)

Online app: confirm identity first, then extract(manual gate). Typing the app uuid wrong will not 404 and will not give any warning; it will only quietly turnanother app 's content into a template, usually discovered only at publish time:

python scripts/acquire.py --list-apps                 # 列租戶下的 app(slug/status/更新時間/uuid)
# ↓ 由用戶執行:對著平台回傳的名稱、slug、uuid、檔數確認「就是這一支」
python scripts/transfer_cli.py confirm-source --slug <slug> --app <uuid_or_slug>
python scripts/acquire.py --slug <slug> --from-app <uuid_or_slug>

acquire --from-app will first verify that this decision exists, then compare the retrieved app id against the one the user confirmed; if they do not match, stop (if you want to change the source, rerun confirm-source).

Repo source: local path or URL both work:

python scripts/acquire.py --slug <slug> --from-repo <path>
python scripts/acquire.py --slug <slug> --from-repo https://github.com/org/repo.git [--ref <branch|tag>]
# 多 app 佈局加 --vfs-subdir;全部偵測不中的自開發佈局:與用戶確認對映後提供 mapping 檔
python scripts/acquire.py --slug <slug> --from-repo <path> --mapping mapping.json

URL sources will git clone --depth 1 to work/<slug>/src_repo/(read-only access, will not push back), the state machine records the commit short hash for reproducibility.Authentication is left to git itself(gh auth login / SSH key / credential helper); do not write tokens into the URL—if you do, the script will mask the userinfo in output and the state file, but it will still remain in your shell history. An unauthorized private repo will fail to clone; set up authentication per the message and rerun.

Only accepts repos shaped like custom apps(iron rule 5): VFS layout detection requires seeing src/main.tsx or src/App.tsx. Ordinary web apps (standalone Next.js/Express/Flutter) can be cloned but will not pass the shape check—that is a rewrite, not a conversion, so explain the exclusion directly to the user.

The three INJ files (data.json/db.json/actions.json) are automatically changed to empty shells; the originals are at work/<slug>/raw/ for Phase 4 reference. Shape check failure (missing entry / SDK file) → discuss with the user; if an SDK file is missing, you can supplement the canonical version from the starter template (this counts as "modification outside the script"; after supplementing, run transfer_cli.py reset --from-stage S1 to go through S1 again).

S1 also inventoriesresources that do not travel with VFS(aligned with builder Phase 0) → work/<slug>/inventory.json:

  • webhook declarations (manifest's "webhook": true + receive_webhook)—external endpoints
  • domains called externally by actions—installing tenants must set the Egress whitelist
  • app schedules (GET /app-crons; cannot be retrieved from repo sources, will be noted for manual confirmation)
  • traces of legacy CustomObject

This inventory will be converted by normalize_meta in Phase 6 into a "post-install setup checklist" written into long_description. The inventory results must be summarized to the user—schedules and webhooks are capabilities the template cannot carry over; failing to explain clearly = silently failing after installation.

Phase 2: Contamination scan (S2)

python scripts/scan.py --slug <slug>

Produces work/<slug>/scan_report.json。reports only, does not modify.

Phase 3: De-tenantization (S3, manual item by item)

  1. Read scan_report item by item, and for each finding draft a decision proposal (decided_by: "proposed") written into decisions.json's findings Zone. Three rulings:
    • replace: exact string old→new (parameterized→ctx.secrets.get + setup_schema; or change to runtime retrieval)
    • delete_file: the entire file is customer-specific clutter
    • keep: confirm harmless (blocker cannot be kept)
  2. Present the full proposal list to the user (file, line, content, action, reason), and ask the user to confirm item by item or as a whole batch.
  3. After the user confirms, change the confirmed entries to decided_by: "user"(only entries the user explicitly agreed to).
  4. Apply:
python scripts/apply_decisions.py --slug <slug> --check   # 先驗完整性
python scripts/apply_decisions.py --slug <slug>           # 套用 + 複掃收斂

When parameterizing, plan setup_schema at the same time: generalize the key naming (example:LINE_CHANNEL_ACCESS_TOKEN, do not URFIT_LINE_TOKEN), and after Phase 4, write it into meta via normalize_meta.

When rewriting external calls, obey iron rule 6: hardcoded third-party endpoints and raw httpx/requests must all be changed to ctx.http.call

def execute(ctx):
    # slug 對應租戶註冊的 EgressService,那裡只有 base_url(domain-only,ADR 0010);
    # 金鑰由 action 自己帶——閘道不注入也不剝除 Authorization。
    token = ctx.secrets.get("EXAMPLE_API_TOKEN")
    resp = ctx.http.call(
        "example-api",
        "/v1/send",
        method="POST",
        body={"text": ctx.params.get("text")},
        headers={"Authorization": f"Bearer {token}"},
    )
    if int(resp.get("status") or 500) >= 400:
        ctx.response.json({"error": "外部服務暫時無法使用", "status": resp.get("status")})
        return
    ctx.response.json(resp.get("data") or {})

ctx.http.call positional argument order is (service, path, method, body, headers, params) ——the SDK goes through RPC as a positional list; if the order is wrong, it does not error, it just stuffs the body into headers. Writing it with named parameters avoids this.

Record the key into setup_schema({"EXAMPLE_API_TOKEN": {"type": "secret", ...}}), record the slug into the "post-install configuration checklist": the tenant must, in the backend /dashboard/settings/integrations withsame-name slug register EgressService (only base_url needs to be filled in), otherwise the action cannot connect out——this is a configuration problem, and cannot be fixed by changing code.required_egress declaration will make the installation flow actively prompt the tenant to complete this step.

When rewriting actions triggered by webhook / schedule,must remain idempotent(the platform is at-least-once, so it may execute repeatedly); for the deduplication key, prefer the business id of the event itself, and next ctx.params["delivery_id"]。

Phase 4: Data Center schema (S4, manual table selection)

python scripts/dc_extract.py --slug <slug> --list          # 列租戶表(* 標記 actions 有引用)
python scripts/dc_extract.py --slug <slug> --tables k1,k2  # 用戶挑定後執行
python scripts/dc_extract.py --slug <slug> --none          # 此 app 無自建表
# 建議加 --ai-go-backend <ai-go repo 路徑> 用權威 parser 驗證
  • Which table belongs to this app and which is shared with the neighboring app——must be judged by the user,--tables is the ruling.
  • Field keys must not use system column names (id/created_at/updated_at); the script will automatically remove them and validate.
  • When the repo source has no online tenant to extract from: derive the table structure from raw/src/data.json and the action code, with --from-file provide a draft table definition, and confirm with the user.

Phase 5: demo data (S5, manual gate)

Draft for business-type templates (integration type may be skipped):

  1. actions/seed_demo_data.py: idempotent (first check whether the main table has data and exit if so), Traditional Chinese Taiwan-localized sample data, relative dates (date.today() + timedelta), use ctx.log() not print(), new-system API (ctx.db.query_table defaults to ascending order, requires explicit "sort": "-created_at")。
  2. register in manifest.json (timeout_ms: 30000)。
  3. small tables can directly add to the table in dc_schema.json seed block (only newly created tables will be populated).
  4. After review by the user, executed by the user:
python scripts/transfer_cli.py gate --slug <slug> --stage S5 --decision approved
# 或不需要 demo 資料:
python scripts/transfer_cli.py gate --slug <slug> --stage S5 --decision skipped --notes "<理由>"

Phase 6: meta and local audit (S6)

python scripts/normalize_meta.py --slug <slug> --name "<名稱>" --category <cat> \
    --description "<一句話>" --author "<作者>" [--setup-schema setup.json] [--tags a,b]
# ↓ 人工閘,由用戶執行:逐項讀過 meta 才放行(AI 可以起草,不能拍板)
python scripts/transfer_cli.py confirm-meta --slug <slug>
python scripts/audit_local.py --slug <slug> [--ai-go-backend <path>]
  • normalize_meta will re-inventory based on the final content: automatically generate the "post-install configuration checklist" into long_description, and automatically declare the remaining ctx.http.call(slug) into required_egress (missing declaration = the tenant is not prompted to authorize at install time, and even if installed it will not run).
  • meta manual gate: name / description / category / tags / long_description are the only façade third parties can see after listing; typos, leftover customer names, and wrong category selection all require resubmission for review. The audit's "meta manual gate" item will block until the user confirms; the ruling is bound to the file hash——if meta is changed again after confirmation, it must be reconfirmed (If rerunning normalize_meta produces exactly the same content, reuse the original confirmation; no need to press again). When reporting to the user, include the full long_description text, don't just report the field names.
  • --ai-go-backend It is recommended to point to ai-go-developer repo (ctx-core's DSL parser has zero dependencies, and it runs the same set as the platform's "validate on save"); pointing to ai-go is also fine (requires that repo's dependencies).
  • actions/_shared/**.py Shared modules do not require execute(ctx)(audit already exempts it).

Only passes if all PASS. If a failed item requires a code change: go back to Phase 3 to supplement the ruling (reset --from-stage S2 then rerun), do not hand-edit.

Phase 7: Developer platform draft creation (S7)

python scripts/devportal.py push --slug <slug>

Create module (platform automatically brings 1.0.0 draft) → push metadata → push files (platform automatically records deploy event) → platform preflight must be ok.push has built-in write-then-read-back (★): metadata key fields (name/category/setup_schema/data_center_schema) and file count are all GET back for comparison, if they don't match it fails — don't claim success just because the API returned 200.

push also pre-validates data_references_schema(GET /refs/available-tables / .../columns): references to tables or fields that don't exist in AI GO are blocked directly, no need to wait until files are pushed to get a preflight fail. These two endpoints are not present in every deployment(authoritative list see GET /dev-docs/endpoints); when unreadable, push prints WARN instead of giving a green light — that means "reference declarations are unverified", and correctness is instead borne by S8.

When there is no editable version(the previous version has been submitted for review or published), push will stop and indicate which one to use:

python scripts/devportal.py withdraw --slug <slug>              # 送審中 → 撤回才能改
python scripts/devportal.py bump --slug <slug> --kind minor     # 已發布 → 開新版本

bump will set the new version as the S7 target andreset S8/S9(the new version hasn't been tested, the old green light cannot be reused). To verify what the platform actually stored, or to take over a template that wasn't generated locally: python scripts/devportal.py pull --slug <slug>。

Module status has an additional unpublished(platform 2026-08-11): a module that was once listed and then delisted has status unpublished rather than draft, andall versions will be set to superseded— so to continue developing after delisting,push will tell you to first bump. Canceling delisting (POST /modules/{id}/restore, the "cancel delisting" on the portal) only changes the status back to draft,does not relist; to relist you can only resubmit for review and approval. withdraw(withdraw submission) and "delisting" are two different things, don't mix them up.

Phase 8: Sandbox end-to-end testing (S8)

python scripts/e2e_devportal.py --slug <slug>              # full(送審必須)
python scripts/e2e_devportal.py --slug <slug> --quick      # 快速檔(迭代中重驗用)

Tiers (aligned with builder's change scope tiers):

Tier Content Purpose
--quick preflight + sandbox secrets + CRUD for each table (insert→list→update→delete) Quick re-verification after only copy/CSS changes; does not record test events, does not advance the state machine
full (default) quick + sandbox egress registration + execution of all enabled actions + seed_demo_data idempotent rerun + test event Required before submission(this skill's requirement, the platform no longer enforces it); S9 will check that the last e2e was full

e2e table CRUD runs on two planes, the two have different endpoints and cannot be cross-wired (details see references/devportal-api.md"the data plane has two sets"):

  • data_center_schema declaredself-built tables → /data-center/tables/{key}/records (old /data/objects/ The surface has been retired along with AI GO, and the platform returns 404; update/delete must include the table name)
  • data_references_schema declaredreferenced tables → /proxy/...(the platform validates against the AI GO snapshot)

Sample rows for referenced tables are generated according to the GET /refs/tables/{t}/columns real column types; Declaring a table that does not exist in AI GO will fail here (rather than being blocked at runtime after publishing). If this endpoint is not deployed here (404), fall back to the seed cycle——POST /sandbox/v/{vid}/tables/{t}/seed let the platform generate sample rows itself, seed→list→query→update→delete, discrimination power is unchanged (if the table does not exist, seed returns 4xx). A 5xx from seed means the platform side errored while generating sample rows; downgrade to read-only verification and log a WARN,write paths unverified must be disclosed to the user。

Submission threshold (relaxed by the platform on 2026-08-04): the only hard threshold left is preflight ok + at least one deploy event for that version(assert_deployed). Preview testing and "at least one success per enabled action" have both been made non-mandatory——the stricter version from 2026-07-28 in practice blocked submissions on a testing ritual (even a bump with no code changes had to be rerun action by action); the platform has rolled it back.

But a relaxed threshold is not a relaxed quality bar. The e2e's submit-gate entries are still reconciled against the server (GET .../events); actions that have not been run are still listed——only the wording changes from "will be blocked by the platform" to "unverified, must be disclosed to the user before submission".Submitting without having run it = publishing code no one has verified, this is gated by this skill's human gate (S9), no longer relying on the platform to block it. You can check the current status separately at any time:

python scripts/devportal.py events --slug <slug>

This means:

  • --expect allow_fail_actions declared actions will not block submission, butmust be named one by one when reporting: which ones were not verified, why (missing real credentials/external service unreachable), and what the risk is.
  • If it truly cannot be run and you do not want to publish with the risk, there are still those two paths: supply real credentials (--secrets-file) and rerun, or set the manifest to is_enabled:false disable it and push again.
  • bump versions produced now automatically record a deploy event (detail.source=bump), and will no longer be stuck at "deployed 0 times" and unable to submit.
  • prod deployment is triggered by the release tag; the live platform may lag behind main——if submission is unexpectedly blocked by a 422 and the message mentions an action not executed, that is the old threshold still live; just rerun according to the message.

Interpretation rules (write into the report, explain item by item when summarizing to the user):

  • A pass under a dummy key does not equal full usability: the action may only reach an early-exit path (e.g. returns immediately on signature verification failure) and return 2xx, while the deeper logic was not executed. When summarizing to the user, every action that passes with dummy values must be marked "shallow pass"; official publishing quality is based on real-value tests.
  • Actions requiring a real third-party connection:credentials always go through --secrets-file(corresponding to setup_schema's key, the action assembles Authorization itself);--egress-file only the real base_url and allow_dynamic_hostof the slug are given——sandbox egress is domain-only, filling in auth_type/auth_config will be blocked by the platform with 400 (iron rule 6). If none of these can be provided, in --expect 's allow_fail_actions declare and explain.
  • runner 503 = the action runner is not enabled on the platform side → log SKIP; clearly mark this risk to the user before submission.
  • approval_status: "pending" / approval exception → log WARN,not a failure, do not retry (Retry = duplicate order creation + duplicate approval form creation).
  • The action declared by the webhook is verified as a general action in the sandbox;external endpoint registration cannot be tested in the sandbox, has been added to the post-installation configuration checklist and explained to the user.
  • By default, test events are automatically recorded (meeting the submission threshold); to go through the most realistic frontend verification, change --no-event, then enable https://developer.ai-go.app/preview/<module_id>?v=<version_id>(auto-records after 3 seconds with no errors).

Phase 9: Submission (S9, manual gate)

Prerequisite: the last e2e must be full (the script will block otherwise). Report the key points of e2e_report.json to the user (especially SKIP/WARN items and the post-installation configuration checklist), and the user executes:

python scripts/devportal.py submit --slug <slug> --note "<給審核者的說明>"

submit has built-in write-then-read-back: confirm the version status has changed to submitted。

Phase 10: Store listing images (S10, does not block release)

The template's card and detail page in the store rely on this set of images to speak. A template without images is just text on the shelf, and clicking in shows no visuals. For the detailed contract, see references/marketplace-images.md。

Standard rules

  1. No listing images → go capture them. Open the developer's "Test & Deploy" preview page (/preview/{module_id}?v={version_id}) and actually run it, capturing and saving screenshots page by page.
  2. If they cannot be captured, it does not block release. The preview won't start, the sandbox has no data, the module itself has no displayable screens— these are all common situations.S10 failure never blocks submission or publishing, it is only called out in the report.
  3. If images exist, no further requirement. The basis for judgment is the actual state on the platform:
    • GET /modules/{id}/images returns an empty array → must try again each time a new version is released
    • already has images → this one passes, and no supplement or re-upload is needed for future new versions (re-uploading only appends, disrupting the order and even hitting the 10-image limit)
  4. Composition: one banner + interface screenshots 01~05(inclusive and above, up to 10 images total).
    • The banner is the one with sequence number 00 , and is also the cover
    • How the banner is generated is not restricted—hand-drawn, design mockup, AI-generated, screenshot collage are all fine, the only requirement is that there must be one image with sequence number 00
    • Interface screenshots start from 01 and increment, recommended 16:9, width ≥ 1366

Filename convention (module_images.py parses accordingly, sequence number determines order):

<slug>-00-<情境詞>主視覺.png     ← banner(封面)
<slug>-01-<畫面概要>.png
<slug>-02-<畫面概要>.png

Operation

python scripts/module_images.py status --slug <slug>              # 先看有沒有圖
python scripts/module_images.py upload --slug <slug> --dir <資料夾>  # 依序上傳
python scripts/module_images.py verify --slug <slug> --dir <資料夾>  # 回讀驗證

upload built-in pre-upload checks (whether the banner is first, image count, size, duplicate sequence numbers), sequential upload and read-back verification, and automatically skips when images already exist.

Three things that bite

  • Only open to modules that are published. Anything unpublished is always 409, so S10 is scheduled after S9; the first version must wait for approval and publishing before uploading.
  • ★Uploads are not retried. When the result is unclear, retrying will produce an extra duplicate image, so on failure always stop, and to start over first clear clear the entire module and re-upload.
  • ★Verification cannot use filenames. AI GO will replace the key with templates/<slug>/<hash>.png, the original filename is not retained; only content comparison (HEAD comparing sizes) verifies accurately.

Verification process quick reference

每次改動 template 內容(回 Phase 3 補裁決後):
  scan 複掃 → apply_decisions → (meta 有動則 normalize_meta → confirm-meta)
  → audit_local → push → e2e --quick
里程碑 / 送審前:
  audit_local 全綠 → push(寫後回讀)→ e2e full(actions + 冪等 + test 事件)
  → 摘報 e2e_report + 安裝後設定清單 → 用戶 submit
上架之後:
  module_images.py status → 沒圖才截圖並 upload → verify(截不出來不擋發版)

Error handling

Any step fails → first fully read out the original error message, then check references/troubleshooting.md the quick reference table, do not guess the fix yourself.

Common status code semantics:401 Authentication invalid (PAT revoked/expired)|403 Permissions (Developer side read_only / AI GO side missing builder.access,no retry, no workaround)| 409 slug name collision or version line conflict|400 metadata validation failure (type contract, category, tags whitelist) and other invalid input|422 submission gate(preflight has fail, or that version has no deployment record)| 503 sandbox runner not configured (platform setting, not a program issue).

metadata errors are 400 not 422(2026-08-12 confirmed by testing against the production platform: PUT metadata of validate_metadata ValueError → 400). When checking the table, don't look at the wrong row.

Egress / permission-type errors = configuration issues: stop changing code immediately, forward the original message to the user, guide them to the backend /dashboard/settings/integrations(or ask the tenant admin/platform admin to handle it).

Reference documents

File Content
references/template-contract.md Template directory layout, meta contract, DSL rules, old/new API comparison, production site behavior and mandatory defenses
references/devportal-api.md Developer platform API subset (authoritative:GET /dev-docs/endpoints)
references/marketplace-images.md Store display images: endpoints, hard limits, order and validation, preview screenshot recipe
references/pollution-signals.md Tenant pollution signals and key points for manual interpretation
references/troubleshooting.md When an error occurs: symptom → cause → handling quick reference table