Skip to content

feat(galadon): add the five-stage engineering loop plugin (0.3.4) - #169

Open
Lumos-789 wants to merge 1 commit into
zai-org:mainfrom
Lumos-789:feat/galadon
Open

Lumos-789 wants to merge 1 commit into
zai-org:mainfrom
Lumos-789:feat/galadon

Conversation

@Lumos-789

@Lumos-789 Lumos-789 commented Nov 4, 2026 •

Copy link
Copy Markdown

Plugin type (required) / 插件类型(必填)

  • UI Plugin / 是 UI 插件 — provides MCP Apps interactive pages / 提供 MCP Apps 交互页面
  • Standard plugin / 普通插件(非 UI) — does not provide MCP Apps interactive pages / 不提供 MCP Apps 交互页面
  • Not applicable / 不适用 — repository-only changes; no plugin content changed / 仅仓库级改动,不涉及插件内容

Affected plugin name(s) / 涉及的插件名称:galadon

What / 改动内容

EN — Adds galadon, a five-stage engineering loop — spec, build, verify, ship, learn — for agent-built projects. The repository is the plugin: a Python CLI plus a skill that keeps the agent on the single next action, with one artifact and one gate per stage.

  • init --layout {none,webapp,service,plugin} --stack {python,node,none} scaffolds a project's internal layering (entry docs, an OpenSpec plan face, a spec/ truth layer, a wiki split, the six .agents/ slots, server+web, scripts/deploy/config, tests, a gitignored out/). The scaffold ships passing tests and real check commands, and non-none layouts also gate the directory contract.
  • Role, template and deploy are explicit ports; the bundle ships a local adapter and an ssh/systemd adapter with release directories, an atomic current/previous switch, a deploy lock, a commit-versioned probe and automatic rollback.
  • ship refuses to report success without a version probe, a production release needs --yes-prod, and teardown only removes a root bootstrap created.
  • galadon.yaml is strict: unknown top-level keys fail loudly, matching the shipped schema.

中文 — 新增 galadon:面向 agent 工程的五阶段循环(规格 / 施工 / 验证 / 发布 / 收口),每阶段一个产物一道闸,agent 永远知道下一步该做什么。内置版式与技术栈脚手架生成器、local 与 ssh/systemd 两个部署适配器(版本目录、原子切换、部署锁、探针与自动回滚),以及严格的配置校验。

Visible behavior and testing / 可见行为与测试

  • python3 -m unittest discover -s tests → 109 passed; node --test tests/test_plugin.mjs → 4 passed (macOS 27.0.1 arm64, Python 3.14.4, Node 24.15.0).
  • Real-host end-to-end in an independent slot (/srv/galadon-relay-e2e, reached over an SSH tunnel, no port exposed): ship PASS with HTTP 200 and a matching commit; a release that exits on start was probed 48 times / 15 s, failed, and auto-rolled back, with the post-rollback recheck green and the bad version left on disk but not serving.
  • The restart window this revision fixes was measured on that host: the service needs ~130–170 ms after systemctl restart before the port accepts.
  • Guards exercised for real: bootstrap refuses a non-empty unmarked root and a root owned by another slot (exit 65, before creating user/unit/caddy); ship refuses a dirty working tree; a second concurrent ship is rejected by the deploy lock (exit 75); ship --dry-run no longer asks for --yes-prod.

Not covered by this round / 本轮未覆盖:

  • No CI in this repository yet; Windows targets and non-systemd init are unexercised. Remote deploys were verified against systemd + Caddy only.

Version and marketplace registration / 版本与市场注册

  • New plugin directory external_plugins/galadon/ (117 files, 192 KiB), .zcode-plugin/plugin.json and the marketplace.json entry both at 0.3.3.
  • Category developer-tools; icon added at the repo-root path the entry uses (assets/galadon/icon.png, 512×512).
  • description_i18n carries en and zh-CN, byte-identical between the manifest and the entry; README.md / README_CN.md are both present and kept in step.

Dependencies, network, permissions, side effects / 依赖、网络、权限与副作用

  • Python 3.10+ and PyYAML for the CLI; git for versioned releases.
  • Remote deploys additionally need SSH, rsync, systemd and Caddy on the target; the ssh adapter writes release directories, a systemd unit, a dedicated system user and a Caddy site block.
  • Local side effects: creates project files under the target root (spec/, wiki/, .agents/, out/) and, on a remote host, /srv/galadon-* plus the unit/user/Caddy block. teardown reverses only what bootstrap created.
  • No telemetry and no third-party network calls: the only network traffic is rsync/ssh to the deploy target.

Licensing / provenance / 许可与来源

  • MIT (LICENSE ships in the plugin directory); code written for this submission.
  • No third-party code or assets bundled; the scaffold templates were extracted from the author's own projects.

Self-check / 自查清单

  • Plugin name is unique kebab-case; new external plugin directory is external_plugins/galadon/; existing fixes retain ownership / 插件名唯一且 kebab-case,外部新增插件位于 external_plugins/galadon/,已有插件修复保留原归属
  • Usage category is set independently of publisher ownership / 用途分类 category 已填写,与维护归属分开:developer-tools
  • Plugin type and affected names are declared above / 已在上方声明插件类型与涉及的插件名称
  • version bumped in BOTH plugin.json and marketplace.json (artifacts are immutable) / plugin.json 与 marketplace.json 两处版本号已同步升级:0.3.3
  • README.md (EN) and README_CN.md (中文) both updated and equivalent / 中英文 README 均已更新且语义一致
  • description_i18n has both en and zh-CN / description_i18n 包含 en 与 zh-CN
  • No secrets, machine-specific paths, or private dependencies / 不含密钥、机器专属路径或私有依赖
  • Model/API/network dependencies documented / 模型、API、网络依赖已写明(见上方依赖小节与两个 README)
  • python3 scripts/validate.py passes on this branch, and the plugin artifact was built through the same build_zip() code path CI uses (size and sha256 in the comment below). The full python3 scripts/build_dist.py needs the repo's UI-plugin pnpm build and fails identically on upstream main (excalidraw: incomplete build), so CI runs it here — see the note above. / 本地 validate.py 通过,本插件产物用 CI 同一条 build_zip() 路径打出(体积与 sha256 见下方评论);完整 build_dist.py 依赖仓库自带 UI 插件的 pnpm build,在 main 上同样失败,交由 CI 执行

@Lumos-789

Copy link
Copy Markdown
Author

0.2.1 — verification (2026-11-04)

Environment: ZCode CLI 0.17.1, macOS 27.0.1 arm64, Python 3.14.4.

Installed from a local marketplace (zcode plugins install galadon@…) → galadon@0.2.1. The skill resolves from the plugin cache at ~/.zcode/cli/plugins/cache/…/galadon/0.2.1/skills/galadon/SKILL.md, not from a repo checkout.

Ran the bundled CLI from that installed cache in an empty directory:

Step Result
bin/galadon init --template api --role solo workspace created, phase: spec, one next action
galadon next prints the single next action for the current phase
galadon check with the template spec (still TODO) FAIL spec/spec.md still contains TODO
galadon check without ## Problem FAIL spec/spec.md is missing ## Problem
galadon check without an acceptance checkbox FAIL needs at least one acceptance checkbox
galadon check with a complete spec PASS, and next advanced the phase

Repo-side gates for the same tree: python3 -m pytest tests -q → 41 passed (+3 subtests), node --test tests/test_plugin.mjs PASS, plugin_acceptance.py PASS galadon@0.2.1, marketplace_check.py OK.

Not covered: remote bootstrap/ship/teardown over SSH (needs a real target host), and --env prod refusal paths — those are exercised by unit tests only.

@Lumos-789

Copy link
Copy Markdown
Author

Addendum — also run inside a real ZCode agent session

Not only the CLI from the cache: a headless ZCode session (sess_e1ac0f61-9da2-405f-b231-a805e1ce6c2f) with the skill forced by its qualified name. The agent found the bundled bin/galadon by itself, ran galadon init --template api --role solo (the first attempt was refused because the directory had auto-generated empty folders — the CLI told it to pass --force, and the agent did), then ran galadon next and reported phase: spec with the single next action. Nothing was run by hand in that session.

@panda920

panda920 commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

✅ Internal validation passed. A ZCode maintainer will review the change next; after acceptance, release checks run separately and this PR is closed automatically.

@Lumos-789 Lumos-789 changed the title feat: add galadon (five-stage engineering loop with deploy ports) feat: add galadon (five-stage engineering loop with layout/stack scaffolding) Nov 4, 2026
@Lumos-789

Copy link
Copy Markdown
Author

Third round — review & simplification (2026-11-05)

No new features. Three things came out of the audit:

1. A documented port did not exist. The README listed four ports — role, template, deploy and tool — and described a local files implementation for the tool port. src/galadon/tools.py contained five protocols with no implementation and no reference anywhere in src/ or tests/. It is gone; the README now says three, matching the code. Three other protocol classes (RolePort, DeployPort, TemplatePort, Gate) and the Ticket.verify per-task override were removed for the same reason: no consumer.

2. 41 byte-identical template copies collapsed into _shared. The three layout profiles carried the same placeholder docs and .gitkeep markers in near-identical copies. They now render from one copy each.

The generated output is byte-identical: I enumerated all 36 profile × layout × stack combinations before the change, re-ran them after, and diffed the trees — the only differences are the two that the change intends (the removed tools: block in galadon.yaml, and a --stack qualifier in the generated layout README). No file was added or removed from any generated project. I also unpacked the published payload on its own and used its init to generate the same combinations, comparing against the repo build: identical.

3. The 0.3.0 capability was missing from every description. plugin.json, .codex-plugin/plugin.json, package.json, pyproject.toml and the skill frontmatter all still described only the five-stage loop, so a marketplace visitor would never learn that init --layout/--stack exists. All five now mention the scaffold generator alongside the five stages.

Also corrected: the layout directory lists in the READMEs and --list-layouts were four separate copies that all omitted src/ (created by the python stack); "init leaves check green" was false for --stack none; the English port table omitted the ssh adapter; the Chinese README said the layering was sampled from one production project — it was three.

Verification: 92 Python tests + 4 Node tests pass; tools/plugin_acceptance.py galadon and the public-release gate are clean; the install → disable → enable → uninstall → reinstall matrix passes for 0.3.0.

Payload: re-synced, 127 files (down from 149 — that is the template de-duplication), verified blob-by-blob against Lumos-789/galadon@dae85a8.

Not verified this round: no end-to-end deploy was run; the change touched templates, docs and unreachable code only.

@panda920

panda920 commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

✅ Internal validation passed. A ZCode maintainer will review the change next; after acceptance, release checks run separately and this PR is closed automatically.

@panda920

panda920 commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

✅ Internal validation passed. A ZCode maintainer will review the change next; after acceptance, release checks run separately and this PR is closed automatically.

@Lumos-789

Copy link
Copy Markdown
Author

Re-pushed the payload at 0.3.1 (5169a21). tools/verify_pr_payload.py galadon:169 → MATCH, 117 files, every blob identical to Lumos-789/galadon@a527132.

Changes since the 0.3.0 payload (884b343):

  • Strict config: galadon.yaml now rejects unknown top-level keys instead of ignoring them. The shipped schema already declared additionalProperties: false, so the retired 0.1.0 tools: block could sit in a real config doing nothing. The error names every unknown key and the allowed set.
  • Docs corrected: with the default none layout the first check failure is the spec gate (spec/spec.md still contains TODO); no checks configured only appears once the spec is complete. README.md, README_CN.md and init --list-layouts had the checks failure described as the first one.
  • Tests 92 → 97 (+ node 4): a standing matrix over all 36 layout × stack × template combinations, asserting each is either green on a fresh scaffold or fails with the specific reason it is expected to produce; and examples/minimal — shipped in the tarball, previously covered by nothing — must now pass galadon check from a scratch copy.
  • PR body updated: it claimed "tool boundaries are explicit ports" (that port was removed in 0.3.0) and never mentioned the --layout/--stack generator.

@Lumos-789

Copy link
Copy Markdown
Author

0.3.2 — packaging hygiene (2026-11-05)

Payload re-pushed at 0.3.2: head 884b343 → 5169a21 → 6210ff6.
tools/verify_pr_payload.py galadon:169 → MATCH, 117 files, every blob identical to Lumos-789/galadon@aecd75a.

What was wrong. Re-running the suite from a clean tree, the bytecode test came up red:

  1. pytest from the repo root roamed into examples/, collected examples/minimal/tests/test_health.py — the 98th test — and imported the example's server.py.
  2. That wrote __pycache__ inside examples/minimal/, a directory that ships in the npm tarball.
  3. package.json files is an allowlist and overrides .npmignore, so the bytecode test's own subject (the .pyc files) rode into the package. The __pycache__/ lines in .npmignore are dead for allowlisted directories.
  4. npm test stayed green because unittest discovery already lives in tests/; the failure surfaced only on the pytest path. So the previous "97 + 4 green" was order-dependent — it held only if nothing had run pytest first.

Fix.

  • pyproject.toml: [tool.pytest.ini_options] testpaths = ["tests"] — pytest stays in tests/. Example coverage is not lost: BundledExampleTests runs the example's own galadon check from a scratch copy.
  • package.json: prepack removes __pycache__ and *.pyc, so the tarball no longer depends on what happened to run in the tree before packing.

Evidence.

Check Result
pytest collected tests 98 → 97 (the extra was the example test)
.pyc under examples/ after a pytest run 0
node --test immediately after pytest 4/4 pass
2 .pyc planted, then npm pack --dry-run tarball bytecode entries 0, disk cleaned
npm test 97 py + 4 node, exit 0
marketplace_check / simplify_gate / public_release_gate OK zcode=6 codex=4 / PASS / PASS

Not covered. No npm publish was run — the tarball claim is verified from npm pack --dry-run file lists only. The .npmignore __pycache__/*.pyc lines were left in place; as noted above they do not protect allowlisted directories, and the guard is now prepack plus the test.

@Lumos-789

Copy link
Copy Markdown
Author

0.3.3 — deploy-path hardening (2026-11-05)

Payload re-pushed at 0.3.3: head 6210ff6 → 81f60a8.
tools/verify_pr_payload.py galadon:169 → MATCH, 117 files, every blob identical to Lumos-789/galadon@0d49ecc.

Everything below came from running the loop end to end against a real host — an isolated slot (/srv/galadon-e2e, own systemd unit, own Caddy marker, own port) with the probe going through an SSH tunnel to loopback, so no port was exposed and nothing was left behind.

1. The probe judged a service that had not started yet. systemctl restart returns once the process is spawned, not once it listens; measured on the host, the port refused connections for 130–170 ms afterwards. The single-shot probe fired inside that window, so both shipments of a real project failed and rolled back (2/2). The same zero tolerance made a rollback that had restored service report itself as failed, because the post-rollback re-check used the same probe. Connection-level failures now retry inside a bounded 15s readiness window (adapters/probe.py, shared by local and ssh); an HTTP answer is final and still goes through the commit check; a target that never answers fails loudly with the attempt count — 48 attempts over 15s in the observed failure.

2. bootstrap adopted directories it did not own. It ran install -d on whatever remote_root it was given and wrote its ownership marker into it, so a directory holding someone else's data became a galadon root — and teardown then deleted it, because the marker it checks was planted by that bootstrap. Reproduced: a foreign important.txt was destroyed. It now refuses a non-empty unmarked directory, and a root carrying another slot's marker, before touching the user, the unit or Caddy.

3. teardown died on a shared run user. The default galadon user is shared by every slot; userdel fails while any other slot still runs as it — the normal state on a host with two deployments — and the failure happened after rm -rf root. It now leaves the user in place, says why, and reports user_skip_reason, while still failing loudly if the user cannot be removed for any other reason.

4. ship stamped HEAD on an artifact built from the working tree. Uncommitted changes shipped under a commit they were not part of, so a later rollback could restore a version that never existed. ship now refuses and names the files, exempting the loop's own generated files (AGENTS.md phase block, evidence, the release record, .galadon/).

5. ship --dry-run --env prod required --yes-prod — previewing a production release demanded the flag that authorises one. Preview is now allowed and prints requires: --yes-prod; a real prod run still requires it.

Scenario (real host) Result
bootstrap twice idempotent — unit_changed: false, changed: false
first ship, empty root PASS, previous empty
second ship, new commit PASS, previous = prior release
broken artifact 48 attempts / 15 s → FAIL, automatic rollback, re-probe HTTP 200
probe_expect unsatisfiable probe body missing expected text: <commit> <marker> → rollback
two concurrent ships second refused exit 75 (deploy lock), first PASS, links consistent
bootstrap into a foreign root refused exit 65, data intact, nothing provisioned
teardown with two slots live exit 0, user_removed: false + user_skip_reason
dirty tree refused, naming src/galadon_relay/app.py
local adapter, full loop check → ship → rollback verified

Tests: 97 → 109 (readiness window, dirty-tree guard, bootstrap/teardown guards, prod preview) plus the node suite 4/4. marketplace_check, simplify_gate and public_release_gate pass; the host was torn down and verified clean afterwards.

A blind spot worth stating. The probe trusts a marker the build writes, so a build that refreshes commit.txt without refreshing the code passes the probe. That is exactly what happened in this project's own dogfood app: cp -r src/x dist/x nests once the destination exists, so from the second build on the top-level package stayed stale while the marker was new. Fixed there (rm -rf dist first), but the general shape remains: only probe_expect — asserting content the new build alone can emit — catches it.

Not covered. No npm publish; the Caddy/DNS front path was not exercised (the original test slot no longer resolves, which is why probes ran over a tunnel); and the rollback chain is one level deep by design — after a rollback previous equals current, deliberately, since pointing it at the failed release would let a second failure roll back onto a known-broken build.

@Lumos-789 Lumos-789 changed the title feat: add galadon (five-stage engineering loop with layout/stack scaffolding) feat(galadon): add the five-stage engineering loop plugin (0.3.3) Nov 5, 2026
@Lumos-789

Copy link
Copy Markdown
Author

Re-pushed this branch as b9eb0a6f and fixed two classes of defect that were ours, not the repository's. Nothing about the plugin's behaviour changed.

1. The PR now follows .github/PULL_REQUEST_TEMPLATE.md. The body had drifted into hand-written prose and lost the two parts a reviewer checks first: the Plugin type declaration and the Self-check list. Both are back, the five sections CONTRIBUTING asks for are filled in (problem / visible behaviour and testing / version and marketplace registration / dependencies and side effects / licensing), and the title carries the (<plugin-name>) scope the template requires for the squash commit on main.

2. The entry named the repo-root assets/galadon/icon.png, but that file was never uploaded with the branch — the catalogue would have shown a broken icon. It is committed now (512×512) and the entry uses the same absolute CDN form as every other icon-bearing entry in marketplace.json.

Re-verified against the branch itself (not a local checkout):

check result
python3 scripts/validate.py @ b9eb0a6f OK: 53 plugin(s) validated
plugin artifact via build_zip() — the same code path CI runs 103331 B, sha256 620d3b0c704b9bea177e5b028ac4d219e2e56bce7d6486b49d1d9edcbe62d0cb
assets/galadon/icon.png present, 512×512
README.md + README_CN.md in the payload both present

Request: the earlier validate status was bound to 81f60a8f, so it is stale after this push. Could you re-run the external validation on b9eb0a6f? The full build_dist.py still needs the repository's UI-plugin pnpm build and fails identically on main (excalidraw: incomplete build; run pnpm build first), which is why I am relying on CI for that step.

中文摘要:本轮只补元数据与载荷完整性 —— 恢复了模板要求的插件类型声明与自查清单、标题加上 (插件名) scope(模板说明该标题会成为 main 上的 squash commit)、把条目引用的根级图标真正提交并以 CDN 绝对地址引用。分支上 validate.py 通过,插件产物按 CI 同一代码路径打包成功(体积与 sha256 见上表)。旧的 validate 状态绑定在 81f60a8f 上已失效,请在 b9eb0a6f 上重跑一次校验。

@Lumos-789 Lumos-789 changed the title feat(galadon): add the five-stage engineering loop plugin (0.3.3) feat(galadon): add the five-stage engineering loop plugin (0.3.4) Nov 8, 2026
@Lumos-789

Copy link
Copy Markdown
Author

Update: payload bumped to 0.3.4 (head 4b0b298). Payload refreshed to the current main repo (byte-identical, verified MATCH). This round is a behavior-preserving cleanup: redundant comments and dead code removed, docs re-aligned with the code. Full test suite, packaging gates (plugin acceptance / simplify / public release) and the install lifecycle matrix all pass locally.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants