diff --git a/.asf.yaml b/.asf.yaml
index 6ac3fe6b8b..453ff07f19 100644
--- a/.asf.yaml
+++ b/.asf.yaml
@@ -62,11 +62,18 @@ github:
required_approving_review_count: 1
required_status_checks:
strict: false
- # test is the single job in .github/workflows/ci.yml, covering lint,
- # format, build, tsc, knip, the selected workspace suites, e2e and
- # Storybook. Renaming it there, or adding a paths filter that stops
- # ci.yml from running, freezes every pull request: the check never
- # reports and no committer can override it.
+ # test is the single unconditional job in .github/workflows/ci.yml. It
+ # runs the install-free contract checks on every change and installs the
+ # toolchain only for the validation its own planning step selects, so a
+ # documentation-only change still reports without paying for a build.
+ # Renaming the job there, adding a paths filter that stops ci.yml from
+ # running, or splitting the work back across jobs so this context comes
+ # from an aggregator that can be skipped, freezes every pull request:
+ # the check never reports and no committer can override it.
+ # A required context must report on every pull request, so a lane
+ # behind a paths filter cannot be listed here: the filter would keep
+ # the workflow from starting and the check would stay pending forever.
+ # windows_recovery is filtered and therefore deliberately absent.
contexts:
- test
@@ -81,9 +88,11 @@ github:
restrict_force_push: true
environments:
- # These environments are the external human gates around signing secrets
- # and npm OIDC. Naming an environment here replaces its settings wholesale,
- # so keep every protection rule in this declarative authority.
+ # These environments are the deployment boundaries around signing secrets
+ # and npm OIDC. Release publication adds human review; scheduled npm
+ # Nightly relies on exact ref admission instead. Naming an environment here
+ # replaces its settings wholesale, so keep every protection rule in this
+ # declarative authority.
release:
required_reviewers:
- id: M4n5ter
@@ -95,7 +104,25 @@ github:
policies:
- name: "v*-incubating-rc*"
type: tag
- npm-release:
+ npm-publication:
+ required_reviewers: []
+ wait_timer: 0
+ prevent_self_review: false
+ deployment_branch_policy:
+ protected_branches: false
+ policies:
+ - name: main
+ type: branch
+ nightly:
+ required_reviewers: []
+ wait_timer: 0
+ prevent_self_review: false
+ deployment_branch_policy:
+ protected_branches: false
+ policies:
+ - name: main
+ type: branch
+ product-release:
required_reviewers:
- id: M4n5ter
type: User
@@ -104,8 +131,8 @@ github:
deployment_branch_policy:
protected_branches: false
policies:
- - name: "v*"
- type: tag
+ - name: main
+ type: branch
notifications:
commits: commits@maka.apache.org
diff --git a/.gitattributes b/.gitattributes
index ded2a4ecf0..1aad5a3cf6 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -10,3 +10,8 @@
/.claude export-ignore
/.maka-shots export-ignore
/maka-proposal-zh-review.txt export-ignore
+
+# DeepSeek Harness remains available to repository development and benchmarks,
+# but its external runtime dependency closure is not an ASF source-release input.
+/packages/eval/harbor/deepseek-harness-toolchain/package.json export-ignore
+/packages/eval/harbor/deepseek-harness-toolchain/package-lock.json export-ignore
diff --git a/.github/ASF_NPM_RELEASE.md b/.github/ASF_NPM_RELEASE.md
index 01b11ad9b5..db621a0c88 100644
--- a/.github/ASF_NPM_RELEASE.md
+++ b/.github/ASF_NPM_RELEASE.md
@@ -72,9 +72,9 @@ registry side effects.
After both source-release votes approve the candidate, the product Release
workflow creates `v` at that approved commit. The npm Stage workflow
-then builds and validates one tarball from that final tag, submits those exact
-bytes to npm staging through the protected `npm-release` Environment and OIDC,
-and records the stage identity. Human approval with npm 2FA makes the package
+then builds and validates one tarball from that final tag in jobs without OIDC. The OIDC job runs
+only reviewed `main` publisher code, submits those exact bytes through the main-restricted
+`npm-publication` Environment, and records both product-source and publisher identities. Human approval with npm 2FA makes the package
public; Finalize verifies the registry bytes, integrity, signature, provenance,
and dist-tag.
diff --git a/.github/ASF_SOURCE_RELEASE.md b/.github/ASF_SOURCE_RELEASE.md
index d936e14637..82d815a1fc 100644
--- a/.github/ASF_SOURCE_RELEASE.md
+++ b/.github/ASF_SOURCE_RELEASE.md
@@ -19,14 +19,9 @@
# Apache Maka source release runbook
-This runbook prepares the official Apache Incubator source-release component.
-The first Maka release also requires npm and Desktop convenience artifacts, but
-those artifacts have separate build, licensing, signing, and acceptance gates.
-They must be built from the exact IPMC-approved source release produced here.
-
-The workflow implements release mechanics; it does not establish that a commit
-is legally ready to release. Before starting a vote, the PPMC and mentors must
-confirm the candidate's provenance and release documents.
+This runbook prepares the official Apache Incubator source release. npm, Desktop, and other convenience artifacts use separate build, licensing, signing, and acceptance processes; they are not part of this source candidate or its release vote. Stable convenience artifacts, when provided, are built from the approved source release and tag.
+
+The workflow implements release mechanics; it does not replace the human review of candidate provenance and release documents performed through the project's release vote.
## Candidate contract
diff --git a/.github/DESKTOP_NIGHTLY.md b/.github/DESKTOP_NIGHTLY.md
new file mode 100644
index 0000000000..854ede602d
--- /dev/null
+++ b/.github/DESKTOP_NIGHTLY.md
@@ -0,0 +1,41 @@
+
+
+# Desktop Nightly
+
+Desktop Nightly is an ephemeral developer snapshot, not an Apache release. It builds the current `main` commit every day so contributors can try recent Desktop changes and report problems without waiting for an ASF source-release vote.
+
+The npm publication workflow gives each snapshot an immutable version such as `0.2.0-dev.42.20260829`. The run number is the sole ordering authority. After that exact npm version is public, it triggers Desktop Nightly with a version-only artifact; the authenticated workflow event supplies the exact source commit and upstream run. Each fresh Desktop Nightly creates a `v` tag protected by the checked-in `Immutable release tags` ruleset and one GitHub draft prerelease containing the macOS and Windows packages, blockmaps, `dev-mac.yml`, `dev.yml`, and one offline Sigstore bundle. The workflow verifies every remote asset before it publishes the prerelease as non-Latest. Packaged Nightlies use the GitHub `dev` channel and verify that downloaded bytes were attested by `.github/workflows/desktop-nightly.yml` on `main`. A formal Desktop build uses the separate stable GitHub Release channel and formal product-release attestation identity.
+
+Nightly currently uses the same application identity as the formal Desktop. Installing it replaces the existing Maka installation rather than creating a second side-by-side app. Its user data remains in the same location. Testers who need the formal build should reinstall that build before returning to the formal channel. Builds previously downloaded from `nightlies.apache.org` do not migrate automatically; testers must install the newest GitHub prerelease once, after which GitHub Nightlies update automatically.
+
+## One-time setup
+
+1. After the checked-in `.asf.yaml` reaches `main`, verify that ASF reconciliation created the `nightly` GitHub Environment with only `main` permitted and no approval gate. Do not maintain that policy manually in GitHub. Configure its macOS signing and notarization secrets: `CSC_LINK`, `CSC_KEY_PASSWORD`, `APPLE_API_KEY`, `APPLE_API_KEY_ID`, and `APPLE_API_ISSUER`. Do not expose these secrets to repository-wide or pull-request workflows.
+2. Configure npm Trusted Publishing for `apache/maka` and `.github/workflows/npm-publication.yml`, restricted to the `npm-publication` Environment and with both `npm publish` and `npm stage publish` allowed. Do not create or store a long-lived npm token.
+3. After npm Trusted Publishing is ready, set `NPM_NIGHTLY_ENABLED` to `true`, run `npm publication` from `main` with `channel=nightly`, and verify the exact npm version and `nightly` dist-tag.
+4. Set `DESKTOP_NIGHTLY_ENABLED` to `true` and manually dispatch a fresh npm Nightly. Confirm that its successful run triggers `Desktop Nightly`. Do not rerun a failed attempt in place.
+5. Verify that `v` points to the exact source SHA and that its GitHub Release is published with Draft off, Prerelease on, Latest off, and exactly the nine expected assets. Install that prerelease on both platforms.
+6. Publish one later fresh Nightly and confirm a GitHub-to-GitHub automatic and differential update on both platforms before sharing the channel with testers.
+
+The npm schedule starts at 18:17 UTC. Before changing the npm tag, the workflow requires its run number to exceed the current `nightly` version. Desktop assembles and verifies a draft before one publish mutation; a packaging, attestation, tag, upload, or digest failure leaves no partially published GitHub Release. Never rerun a failed workflow attempt in place; dispatch a fresh npm Nightly with a newer version.
+
+GitHub Release retention is intentionally outside this workflow. Do not delete an old Nightly prerelease or its tag while any installed client may need its payload or blockmap. One Nightly is additionally pinned by tag, asset name, and SHA-256 in `scripts/windows-upgrade-baseline.json` as the Windows upgrade gate's baseline: deleting that prerelease fails the gate on every pull request that touches the Windows release path until the pin moves to another published build. Disabling `DESKTOP_NIGHTLY_ENABLED` stops new Desktop publication without mutating tags or releases.
+
+Remote Runtime Host setup uses the exact `maka-agent@` package embedded in the Desktop manifest. The npm package is verified before Desktop artifacts become visible, so clean remote setup never depends on an unpublished Runtime Host version.
diff --git a/.github/RELEASE_CHECKLIST.md b/.github/RELEASE_CHECKLIST.md
index 5a1c32ab00..4402722337 100644
--- a/.github/RELEASE_CHECKLIST.md
+++ b/.github/RELEASE_CHECKLIST.md
@@ -19,9 +19,10 @@
# Product release checklist
-The `Release` workflow is Maka's convenience-artifact release entry point. Desktop and CLI/TUI are
+The IPMC-approved source archive on ASF distribution infrastructure is the Apache release. The
+`Release` workflow is Maka's convenience-artifact distribution entry point. Desktop and CLI/TUI are
built from the exact IPMC-approved ASF source candidate commit. They share that source commit, the
-root product version, one convenience tag, one GitHub Release, one Draft decision, and one release
+root product version, one convenience tag, one GitHub Release, one Draft decision, and one distribution
gate. The workflow creates no Draft until every required artifact job succeeds.
Phase 1 requires:
@@ -31,9 +32,9 @@ Phase 1 requires:
- the signed, notarized, relocatable Apple Silicon CLI/TUI ZIP;
- checksums generated after each artifact reaches its final form.
-The ASF Desktop artifacts must not contain a Git runtime, a bundled-Git manifest, or Git/Dugite
-redistribution notices. Managed-workspace execution remains unavailable until a separately reviewed,
-ASF-compatible verified runtime is connected before admission/T1.
+The convenience Desktop artifacts must not contain a Git runtime, a bundled-Git manifest, or Git/Dugite
+redistribution notices. The retired Git executable-backed managed-workspace path must not be restored;
+future workspace execution requires a separately reviewed Gitoxide production composition before admission/T1.
The first product release also requires the exact `maka-agent@` npm package. The product
tag and Draft must exist before npm staging, but the Draft must remain unpublished until npm is
@@ -60,10 +61,17 @@ must never be exposed to fork or ordinary pull-request jobs.
Before the first product release, confirm the checked-in `.asf.yaml` has reconciled the live repository:
- the `Immutable release tags` ruleset blocks updates, force-pushes, and deletions of `v*` tags;
-- the `release` and `npm-release` Environments accept only their declared tag patterns and require a reviewer other than the triggering user;
-- enable immutable releases so assets and the associated tag cannot change after publication.
-
-These controls close the check-to-upload and check-to-stage windows. Keep the Release in Draft while assets and acceptance are incomplete; publishing early must make subsequent mutation fail closed.
+- the `release` Environment accepts only its declared source-candidate tag pattern and requires a
+ reviewer other than the triggering user;
+- `npm-publication`, `nightly`, and `product-release` accept only `main`; `product-release` requires
+ a reviewer other than the triggering user. `npm-publication` and `nightly` have no GitHub approval
+ gate because scheduled npm and Desktop Nightly publication is automatic; formal npm publication
+ still requires human 2FA approval after staging.
+
+These controls close the check-to-upload and check-to-stage windows. Finalize uses GitHub Actions
+OIDC rather than a stored signing key to attest every convenience artifact. Keep the Release in
+Draft while assets and acceptance are incomplete; Desktop rejects downloaded updates whose exact
+bytes and expected filename are not covered by that protected workflow identity.
## Create the complete Draft
@@ -79,8 +87,8 @@ These controls close the check-to-upload and check-to-stage windows. Keep the Re
5. Confirm `release-identity`, both Desktop matrix entries, `cli-macos-arm64`, and
`publish` pass. A skipped or failed required job must prevent Draft creation.
6. Confirm one Draft named `v` targets the approved source SHA, identifies the ASF source
- reference in its notes, is marked as a GitHub prerelease exactly when the product version is a
- prerelease, is not marked Latest while it remains a Draft, and contains exactly the manifest
+ reference in its notes, is not marked as a GitHub prerelease or Latest while it remains a Draft,
+ and contains exactly the manifest
reported by `node scripts/product-release-artifacts.mjs list`. The manifest covers both Desktop
platforms and update metadata, the standalone CLI/TUI, and their required checksums.
7. Inspect the CLI ZIP. It must contain `bin/maka`, `RELEASE.json`, `DISCLAIMER-WIP`, `LICENSE`, `NOTICE`,
@@ -106,17 +114,22 @@ then rerun. If only the tag exists, the retry creates the missing Draft.
Follow [the npm release runbook](../docs/cli-npm-release.md) against the exact product tag and Draft:
-1. Run **Stage CLI npm release** from `v` and record its successful run ID and attempt.
+1. Record the successful **Release** workflow run ID and attempt that built the Draft assets. Run
+ **npm publication** with `channel=formal` from `main` and record its successful run ID and
+ attempt.
2. Inspect the staged tarball and provenance, then approve that exact stage with npm 2FA.
-3. Run **Finalize CLI npm channel** from `main` and confirm it verifies the public package bytes,
- provenance, signature, and release dist-tag.
+3. Run **Finalize product release** from `main`. Its first job verifies the public package
+ bytes, provenance, signature, and release dist-tag.
4. Install the exact public version on each release platform and complete the npm acceptance steps.
-Keep the GitHub Release in Draft throughout this sequence. A failed or rejected npm candidate
-requires a new product version; never publish the Draft to work around npm state.
-
-When every npm and cross-machine acceptance check has passed, publish the Draft. Mark a stable
-release as Latest at that final publication boundary; prereleases must remain non-Latest.
+Keep the GitHub Release in Draft throughout this sequence. The final workflow job waits at the
+`product-release` Environment. Approve it only after every npm and cross-machine acceptance check
+has passed. It verifies the live Draft digests against the immutable publication record from the
+exact successful Release run, creates Sigstore provenance and an offline
+`Maka--attestation.sigstore.json` bundle, then publishes the convenience Release and makes a
+stable release Latest in the same GitHub operation. Do not publish or
+change the Latest designation manually. A failed or rejected npm candidate requires a new product
+version; never publish the Draft to work around npm state.
## Acceptance on another Apple Silicon Mac
@@ -155,8 +168,20 @@ Download the installer, Windows Desktop ZIP, and both checksum files through a b
7. Add a clean remote Runtime Host from the packaged Desktop app. Confirm setup installs the exact
public `maka-agent@` package and the remote session completes one model turn.
-Immediately before publication, reverify that the approved ASF candidate tag and convenience
-`v` tag still resolve to the same recorded commit. Publish only after npm Finalize and both
-independent-machine acceptance passes. If any required artifact, npm step, or
+Immediately before approving the `product-release` Environment, reverify that the approved ASF
+candidate tag and convenience `v` tag still resolve to the same recorded commit. Approve
+only after npm verification and both independent-machine acceptance passes. If any required artifact, npm step, or
acceptance step fails, keep the Draft unpublished, fix the issue, increment the root product
version, and run the full workflow again. Never replace an existing release identity.
+
+After Finalize publishes the convenience Release, download its attestation bundle and verify each
+installer or archive independently:
+
+```sh
+gh attestation verify path/to/Maka--mac-arm64.zip \
+ --bundle path/to/Maka--attestation.sigstore.json \
+ --repo apache/maka \
+ --signer-workflow apache/maka/.github/workflows/release-cli-finalize.yml
+```
+
+Desktop performs the same trust decision before exposing a downloaded update for installation.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f3367e20fe..155333837e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,11 +32,22 @@ permissions:
contents: read
jobs:
- # Planning and every selected Linux surface share one runner, so the core
- # workflow consumes one automatic job without dropping affected coverage.
+ # One job, not three. A runner slot is scarcer than a minute on shared
+ # infrastructure, and splitting planning from validation made every pull
+ # request queue for a runner three times to reach one verdict. Planning is
+ # the first step; every later step gates on its outputs, so a
+ # documentation-only change costs one short allocation rather than two.
+ #
+ # The name is `test` because that is the required context in `.asf.yaml`.
+ # Renaming it would leave that check unreported on every open pull request
+ # until the rename merged, and nothing could merge while it was unreported.
test:
- runs-on: ubuntu-latest
- timeout-minutes: 120
+ # Pinned, not `ubuntu-latest`. The two resolve to the same image, but only
+ # the alias makes this required context wait at the tail, and the steps
+ # below already assume this image. `ci-workflow-policy.test.mjs` holds the
+ # rule for every lane.
+ runs-on: ubuntu-24.04
+ timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
@@ -49,7 +60,7 @@ jobs:
BASE_SHA: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event_name == 'push' && github.sha || github.event.pull_request.head.sha }}
run: |
- # PR checks may predate later main changes, so the single core job also
+ # PR checks may predate later main changes, so the planning lane also
# validates the exact merged delta. Dispatches and unavailable history
# fail safe to every surface.
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]] || [[ "$BASE_SHA" =~ ^0+$ ]] || ! git cat-file -e "${BASE_SHA}^{commit}"; then
@@ -59,7 +70,20 @@ jobs:
fi
- name: Test CI planner
- run: node --test --test-concurrency=1 scripts/ci-test-plan.test.mjs scripts/verify-windows-harness.test.mjs
+ run: >-
+ node --test --test-concurrency=1 scripts/ci-test-plan.test.mjs
+ scripts/ci-workflow-policy.test.mjs scripts/verify-windows-harness.test.mjs
+
+ # Pure Node like the planner test, and the labelling workflow imports this
+ # module directly, so a tier or exclusion change is caught here rather
+ # than by mislabelling live pull requests.
+ - name: Test PR effort classification
+ run: node --test --test-concurrency=1 scripts/pr-effort.test.mjs
+
+ # The scheduled lifecycle workflow imports this pure policy module. Keep
+ # its time boundaries and exemptions deterministic before it can write.
+ - name: Test issue and PR lifecycle policy
+ run: node --test --test-concurrency=1 scripts/issue-pr-lifecycle.test.mjs
# Same shape and the same needs: a regenerate-and-diff contract that runs
# on Node alone, so it belongs beside the planner test rather than behind
@@ -78,8 +102,26 @@ jobs:
- name: Test the epoch guard
run: node --test --test-concurrency=1 scripts/protocol-epoch-check.test.mjs
- - name: Test AX tree audit contract
- run: node --test scripts/ax-tree-audit.test.mjs
+ - name: Test Computer Use script contracts
+ run: node --test scripts/ax-tree-audit.test.mjs scripts/computer-use/lab-root.test.mjs
+
+ - name: Test script entrypoint contracts
+ run: node --test scripts/script-entrypoints.test.mjs
+
+ # The packaged Windows lane drives the updater through this wiring. It
+ # is a few lines, so it is asserted here on every change rather than by
+ # naming its module in that lane's 25-minute path filter.
+ - name: Test the packaged update feed wiring
+ run: node --test --test-concurrency=1 scripts/update-test-feed-wiring.test.mjs
+
+ # Install-free like its neighbours: the gate reads one source file and
+ # compares it to a hand-edited inventory, so a hook that silently widens
+ # its scope to the whole tree fails here rather than in a profile (#4109).
+ - name: Check the hooks scoped to the whole shell
+ run: npm run check:app-shell-hooks
+
+ - name: Test the app-shell hook gate
+ run: node --test --test-concurrency=1 scripts/check-app-shell-hooks.test.mjs
- name: Verify ASF npm preflight policy
run: npm run check:asf-npm
@@ -90,8 +132,10 @@ jobs:
- name: Check ASF source headers
run: npm run check:asf-headers
+ # Everything below needs an installed toolchain. A plan that selected no
+ # surface stops at the install-free checks above.
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- if: steps.plan.outputs.code == 'true' || steps.plan.outputs.astryx_surface == 'true' || steps.plan.outputs.asf_source == 'true' || steps.plan.outputs.cli_package == 'true' || steps.plan.outputs.release_contract == 'true'
+ if: steps.plan.outputs.heavy == 'true'
with:
node-version: '24'
cache: npm
@@ -109,13 +153,13 @@ jobs:
restore-keys: electron-${{ runner.os }}-
- name: Install Linux runtime dependencies
- if: steps.plan.outputs.runtime_sandbox == 'true'
+ if: steps.plan.outputs.runtime_sandbox == 'true' || steps.plan.outputs.state_root_compat == 'true'
run: sudo apt-get update && sudo apt-get install -y ripgrep bubblewrap
# Ubuntu 24.04 hosted runners gate unprivileged user namespaces through
# AppArmor, which otherwise makes bwrap fail while configuring loopback.
- name: Enable bubblewrap user namespaces
- if: steps.plan.outputs.runtime_sandbox == 'true'
+ if: steps.plan.outputs.runtime_sandbox == 'true' || steps.plan.outputs.state_root_compat == 'true'
run: |
if [[ -e /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]]; then
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
@@ -125,9 +169,15 @@ jobs:
fi
- name: Install dependencies
- if: steps.plan.outputs.code == 'true' || steps.plan.outputs.asf_source == 'true' || steps.plan.outputs.cli_package == 'true' || steps.plan.outputs.release_contract == 'true'
+ if: steps.plan.outputs.code == 'true' || steps.plan.outputs.astryx_surface == 'true' || steps.plan.outputs.asf_source == 'true' || steps.plan.outputs.cli_package == 'true' || steps.plan.outputs.release_contract == 'true'
run: npm ci
+ - name: Check localized TUI copy boundaries
+ if: steps.plan.outputs.code == 'true'
+ run: |
+ npm run check:tui-copy
+ node --test scripts/check-tui-copy.test.mjs
+
# The header audit above remains install-free. The complete source gate
# also exercises generation and therefore runs after its pinned formatter
# dependency is installed, matching the source-candidate workflow.
@@ -135,6 +185,20 @@ jobs:
if: steps.plan.outputs.asf_source == 'true'
run: npm run check:asf-source
+ # Parsed dependency rules and an exact legacy-debt ledger keep the
+ # renderer root from absorbing new feature or Desktop ownership while
+ # the existing AppShell is migrated behind stable boundaries.
+ - name: Check renderer architecture
+ if: steps.plan.outputs.code == 'true'
+ env:
+ BASE_SHA: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
+ run: |
+ if [[ -n "$BASE_SHA" && ! "$BASE_SHA" =~ ^0+$ ]]; then
+ npm run check:renderer-architecture -- --base "$BASE_SHA"
+ else
+ npm run check:renderer-architecture
+ fi
+
- name: Lint
if: steps.plan.outputs.code == 'true'
run: npm run lint
@@ -149,7 +213,9 @@ jobs:
# directly instead of being hidden behind an earlier compilation failure.
- name: Astryx surface inventory
if: steps.plan.outputs.code == 'true' || steps.plan.outputs.astryx_surface == 'true'
- run: npm run astryx:surface-inventory
+ run: |
+ npm run astryx:surface-inventory
+ npm run astryx:surface-inventory:test
- name: Build
if: steps.plan.outputs.code == 'true' || steps.plan.outputs.cli_package == 'true' || steps.plan.outputs.release_contract == 'true'
@@ -163,6 +229,17 @@ jobs:
if: steps.plan.outputs.code == 'true'
run: npm run typecheck
+ # Two drift contracts over the shipped app-icon artwork, sitting beside
+ # the theme drift check for the same reason: the committed bytes are a
+ # build output that nothing else re-derives, so without this a change to
+ # the generator, to the `APP_ICONS` catalog, or to the packaged-resource
+ # list can go green while the artwork it names no longer matches. Those
+ # are the inputs it selects on; regenerating the artwork costs about a
+ # minute, and every other code change used to pay it.
+ - name: App icon artwork drift
+ if: steps.plan.outputs.app_icons == 'true'
+ run: node --test scripts/verify-packaged-app-icons.test.mjs scripts/generate-app-icons.test.mjs
+
- name: Astryx theme drift
if: steps.plan.outputs.code == 'true'
run: npm run astryx:theme -- --check
@@ -208,6 +285,49 @@ jobs:
if: steps.plan.outputs.runtime_host == 'true'
run: npm --workspace @maka/runtime-host run test:dist
+ # A published predecessor writes the durable state; the workspace built
+ # above reads it. Release packaging is deliberately not in front of this:
+ # it takes minutes and changes nothing about whether these decoders can
+ # read that state. The release lanes still qualify exact tarballs.
+ - id: forward-roll-baseline
+ name: Resolve the published forward-roll baseline
+ if: steps.plan.outputs.state_root_compat == 'true'
+ run: node scripts/release-cli-publication.mjs resolve-nightly-predecessor "$GITHUB_OUTPUT"
+
+ - name: Download the forward-roll baseline
+ if: steps.plan.outputs.state_root_compat == 'true'
+ env:
+ SOURCE_URL: ${{ steps.forward-roll-baseline.outputs.tarball_url }}
+ SOURCE_INTEGRITY: ${{ steps.forward-roll-baseline.outputs.integrity }}
+ run: |
+ set -euo pipefail
+ source_path="$RUNNER_TEMP/forward-roll-source.tgz"
+ curl --fail --location --max-filesize 67108864 --proto '=https' --tlsv1.2 \
+ --retry 3 --retry-connrefused --retry-delay 2 "$SOURCE_URL" --output "$source_path"
+ node - "$source_path" "$SOURCE_INTEGRITY" <<'NODE'
+ const { createHash } = require('node:crypto');
+ const { readFileSync } = require('node:fs');
+ const bytes = readFileSync(process.argv[2]);
+ const actual = `sha512-${createHash('sha512').update(bytes).digest('base64')}`;
+ if (actual !== process.argv[3]) throw new Error('Forward-roll baseline integrity mismatch');
+ NODE
+ {
+ echo "FORWARD_ROLL_SOURCE=$source_path"
+ echo "FORWARD_ROLL_SOURCE_SHA256=$(sha256sum "$source_path" | cut -d ' ' -f 1)"
+ } >> "$GITHUB_ENV"
+
+ - name: Qualify durable state against the published baseline
+ if: steps.plan.outputs.state_root_compat == 'true'
+ env:
+ MAKA_QUALIFICATION_BWRAP_USE_SUDO: '1'
+ run: |
+ set -o pipefail
+ npm run --silent release:cli:qualify-state-root -- \
+ --source "$FORWARD_ROLL_SOURCE" \
+ --source-sha256 "$FORWARD_ROLL_SOURCE_SHA256" \
+ --target-workspace "$PWD" \
+ | tee "$RUNNER_TEMP/durable-state-report.json"
+
- name: Ensure xvfb
if: steps.plan.outputs.e2e == 'true'
run: command -v xvfb-run >/dev/null 2>&1 || { sudo apt-get update && sudo apt-get install -y xvfb; }
@@ -238,10 +358,61 @@ jobs:
if: steps.plan.outputs.storybook == 'true'
run: npm --workspace @maka/desktop run smoke:storybook
+ - name: Update stable Rust for CLI packaging
+ if: steps.plan.outputs.cli_package == 'true'
+ run: rustup update stable --no-self-update
+
+ - id: cli-rustc
+ name: Resolve CLI Rust cache version
+ if: steps.plan.outputs.cli_package == 'true'
+ shell: bash
+ run: |
+ echo "version=$(rustc --version | cut -d ' ' -f 2)" >> "$GITHUB_OUTPUT"
+ echo "revision=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
+ {
+ echo "KACHE_CACHE_DIR=${{ runner.temp }}/kache-cli-package"
+ echo "KACHE_RUNTIME_DIR=${{ runner.temp }}/kache-cli-package-runtime"
+ echo "RUSTC_WRAPPER=kache"
+ } >> "$GITHUB_ENV"
+
+ - name: Install Kache for CLI packaging
+ if: steps.plan.outputs.cli_package == 'true'
+ uses: taiki-e/install-action@1ed6d7be6168f6c9046541087ff549b6bc581fdf # v2
+ with:
+ tool: kache@0.16.0
+
+ - id: cli-kache-cache
+ name: Restore CLI Rust build cache
+ if: steps.plan.outputs.cli_package == 'true'
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: ${{ runner.temp }}/kache-cli-package
+ key: kache-runtime-host-peer-cli-package-v0.16.0-${{ runner.os }}-${{ runner.arch }}-rust-${{ steps.cli-rustc.outputs.version }}-${{ steps.cli-rustc.outputs.revision }}
+ restore-keys: |
+ kache-runtime-host-peer-cli-package-v0.16.0-${{ runner.os }}-${{ runner.arch }}-rust-${{ steps.cli-rustc.outputs.version }}-
+
+ - name: Install cargo-deny for CLI packaging
+ if: steps.plan.outputs.cli_package == 'true'
+ uses: taiki-e/install-action@1ed6d7be6168f6c9046541087ff549b6bc581fdf # v2
+ with:
+ tool: cargo-deny@0.20.2
+
- name: Build CLI release candidate
if: steps.plan.outputs.cli_package == 'true'
- run: npm run release:cli:pack
+ run: npm run release:cli:pack -- --allow-dirty
+
+ - name: Report CLI Rust build cache
+ if: steps.plan.outputs.cli_package == 'true'
+ shell: bash
+ run: kache report --format github >> "$GITHUB_STEP_SUMMARY"
+
+ - name: Save CLI Rust build cache
+ if: steps.plan.outputs.cli_package == 'true' && github.ref_name == github.event.repository.default_branch
+ uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: ${{ runner.temp }}/kache-cli-package
+ key: ${{ steps.cli-kache-cache.outputs.cache-primary-key }}
- name: Validate installed CLI release candidate
if: steps.plan.outputs.cli_package == 'true'
- run: npm run release:cli:smoke
+ run: npm run release:cli:smoke -- packages/cli/release/*.tgz
diff --git a/.github/workflows/cli-package-validation.yml b/.github/workflows/cli-package-validation.yml
index 995e49b050..efd62c0b0b 100644
--- a/.github/workflows/cli-package-validation.yml
+++ b/.github/workflows/cli-package-validation.yml
@@ -18,6 +18,33 @@
name: CLI package validation
on:
+ pull_request:
+ paths:
+ - '.github/workflows/cli-package-validation.yml'
+ - '.github/workflows/npm-publication.yml'
+ - '.github/workflows/runtime-host-peer-admission.yml'
+ - 'deny.toml'
+ - 'native/runtime-host-peer/**'
+ - 'package-lock.json'
+ - 'packages/cli/RUNTIME_HOST_PEER_*'
+ - 'packages/cli/src/cli-core.ts'
+ - 'packages/cli/src/runtime-host-cli.ts'
+ - 'packages/cli/src/runtime-host-peer-*'
+ - 'packages/cli/src/runtime-host-service-*'
+ - 'packages/runtime-host/package.json'
+ - 'packages/runtime-host/src/client/peer-client.ts'
+ - 'packages/runtime-host/src/peer-mesh/**'
+ - 'packages/runtime-host/src/server/peer-listener.ts'
+ - 'packages/runtime-host/src/transport/peer-native.ts'
+ - 'packages/storage/package.json'
+ - 'packages/storage/src/file-lifetime-owner.ts'
+ - 'packages/storage/src/native-file-lock.ts'
+ - 'scripts/generate-runtime-host-peer-*'
+ - 'scripts/release-cli-package.mjs'
+ - 'scripts/qualify-released-cli-state-root.mjs'
+ - 'scripts/qualify-released-cli-state-root.test.mjs'
+ - 'scripts/released-cli-state-root-fixture.mjs'
+ - 'scripts/smoke-release-cli-package.mjs'
workflow_call:
inputs:
source_commit:
@@ -25,6 +52,11 @@ on:
required: false
type: string
default: ''
+ package_version:
+ description: Exact Product Nightly version; empty builds the checked-in formal version
+ required: false
+ type: string
+ default: ''
outputs:
release_candidate_artifact_id:
description: Immutable artifact produced by the build job
@@ -32,6 +64,15 @@ on:
release_candidate_run_attempt:
description: Workflow attempt that built the immutable artifact
value: ${{ jobs.build.outputs.release_candidate_run_attempt }}
+ release_predecessor_version:
+ description: Exact npm Nightly version qualified against this candidate
+ value: ${{ jobs.build.outputs.release_predecessor_version }}
+ release_predecessor_tarball_url:
+ description: Exact npm Nightly tarball qualified against this candidate
+ value: ${{ jobs.build.outputs.release_predecessor_tarball_url }}
+ release_predecessor_integrity:
+ description: npm SHA-512 integrity of the Nightly tarball qualified against this candidate
+ value: ${{ jobs.build.outputs.release_predecessor_integrity }}
workflow_dispatch:
permissions:
@@ -39,15 +80,128 @@ permissions:
concurrency:
group: cli-package-validation-${{ github.workflow }}-${{ github.ref }}
+ # Four build targets and four install environments fan out to about fourteen
+ # jobs per run, which makes this the largest holder of runner slots in the
+ # repository. Without this, a run a newer push had already invalidated was not
+ # cancelled — it ran to completion while the replacement queued behind it in
+ # this same group. Release callers arrive through `workflow_call`, where
+ # `github.event_name` is the caller's, so publication runs never cancel.
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
+ peer-native:
+ name: Build direct-peer addon (${{ matrix.target }})
+ runs-on: ${{ matrix.runner }}
+ timeout-minutes: 45
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - runner: macos-15
+ target: darwin-arm64
+ rust_target: ''
+ - runner: ubuntu-24.04-arm
+ target: linux-arm64
+ rust_target: aarch64-unknown-linux-gnu.2.28
+ - runner: ubuntu-24.04
+ target: linux-x64
+ rust_target: x86_64-unknown-linux-gnu.2.28
+ - runner: windows-2025
+ target: win32-x64
+ rust_target: ''
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ inputs.source_commit || github.sha }}
+ persist-credentials: false
+ - name: Update stable Rust
+ run: rustup update stable --no-self-update
+ - id: rustc
+ name: Resolve Rust cache version
+ shell: bash
+ run: |
+ echo "version=$(rustc --version | cut -d ' ' -f 2)" >> "$GITHUB_OUTPUT"
+ echo "revision=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
+ {
+ echo "KACHE_CACHE_DIR=${{ runner.temp }}/kache"
+ echo "KACHE_RUNTIME_DIR=${{ runner.temp }}/kache-runtime"
+ echo "RUSTC_WRAPPER=kache"
+ } >> "$GITHUB_ENV"
+ - name: Install Kache
+ uses: taiki-e/install-action@1ed6d7be6168f6c9046541087ff549b6bc581fdf # v2
+ with:
+ tool: kache@0.16.0
+ - id: kache-cache
+ name: Restore Rust build cache
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: ${{ runner.temp }}/kache
+ key: kache-runtime-host-peer-release-${{ matrix.target }}-v0.16.0-${{ runner.os }}-${{ runner.arch }}-rust-${{ steps.rustc.outputs.version }}-${{ steps.rustc.outputs.revision }}
+ restore-keys: |
+ kache-runtime-host-peer-release-${{ matrix.target }}-v0.16.0-${{ runner.os }}-${{ runner.arch }}-rust-${{ steps.rustc.outputs.version }}-
+ - name: Install the Linux baseline linker
+ if: startsWith(matrix.target, 'linux-')
+ uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
+ with:
+ version: 0.16.0
+ - name: Install cargo-zigbuild
+ if: startsWith(matrix.target, 'linux-')
+ uses: taiki-e/install-action@1ed6d7be6168f6c9046541087ff549b6bc581fdf # v2
+ with:
+ tool: cargo-zigbuild@0.23.2
+ - name: Build the release addon
+ env:
+ MAKA_RUNTIME_HOST_PEER_CARGO_SUBCOMMAND: ${{ matrix.rust_target && 'zigbuild' || '' }}
+ MAKA_RUNTIME_HOST_PEER_CARGO_TARGET: ${{ matrix.rust_target }}
+ run: node native/runtime-host-peer/build.mjs
+ - name: Report Rust build cache
+ shell: bash
+ run: kache report --format github >> "$GITHUB_STEP_SUMMARY"
+ - name: Save Rust build cache
+ if: github.ref_name == github.event.repository.default_branch
+ uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: ${{ runner.temp }}/kache
+ key: ${{ steps.kache-cache.outputs.cache-primary-key }}
+ - name: Enforce the Linux glibc baseline
+ if: startsWith(matrix.target, 'linux-')
+ run: |
+ node - <<'NODE'
+ const { execFileSync } = require('node:child_process');
+ const output = execFileSync('readelf', [
+ '--version-info',
+ 'native/runtime-host-peer/target/release/maka_runtime_host_peer.node',
+ ], { encoding: 'utf8' });
+ const versions = [...output.matchAll(/GLIBC_(\d+)\.(\d+)/g)].map((match) => [
+ Number(match[1]),
+ Number(match[2]),
+ ]);
+ const newer = versions.find(([major, minor]) => major > 2 || (major === 2 && minor > 28));
+ if (newer) throw new Error(`Direct-peer addon requires GLIBC_${newer.join('.')}`);
+ NODE
+ - name: Stage the platform addon
+ env:
+ PEER_TARGET: ${{ matrix.target }}
+ run: node -e "const fs=require('node:fs'),p=require('node:path'); const d=p.join('peer-prebuilds',process.env.PEER_TARGET); fs.mkdirSync(d,{recursive:true}); fs.copyFileSync(p.join('native','runtime-host-peer','target','release','maka_runtime_host_peer.node'),p.join(d,'maka_runtime_host_peer.node'))"
+ - name: Upload the platform addon
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: runtime-host-peer-${{ matrix.target }}
+ path: peer-prebuilds
+ if-no-files-found: error
+ retention-days: 1
+
build:
name: Build immutable tarball
+ needs: peer-native
runs-on: ubuntu-24.04
- timeout-minutes: 60
+ timeout-minutes: 45
outputs:
release_candidate_artifact_id: ${{ steps.release-candidate.outputs.artifact-id }}
release_candidate_run_attempt: ${{ github.run_attempt }}
+ release_predecessor_version: ${{ steps.predecessor.outputs.version }}
+ release_predecessor_tarball_url: ${{ steps.predecessor.outputs.tarball_url }}
+ release_predecessor_integrity: ${{ steps.predecessor.outputs.integrity }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
@@ -59,7 +213,26 @@ jobs:
cache: npm
- name: Select the release npm toolchain
run: npm install --global --no-audit --no-fund "$(node -p 'require("./package.json").packageManager')"
+ # Resolving one npm version is seconds of work against a registry this
+ # job already reaches, so it rides the runner that waits on the addon
+ # builds instead of holding a second one for the whole wait.
+ - name: Resolve the current npm Nightly as immutable evidence
+ id: predecessor
+ run: node scripts/release-cli-publication.mjs resolve-nightly-predecessor "$GITHUB_OUTPUT"
+ - name: Install cargo-deny
+ uses: taiki-e/install-action@1ed6d7be6168f6c9046541087ff549b6bc581fdf # v2
+ with:
+ tool: cargo-deny@0.20.2
+ - name: Download direct-peer addons
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ pattern: runtime-host-peer-*
+ path: ${{ runner.temp }}/runtime-host-peer-prebuilds
+ merge-multiple: true
- name: Build the release tarball once
+ env:
+ MAKA_CLI_NIGHTLY_VERSION: ${{ inputs.package_version }}
+ MAKA_RUNTIME_HOST_PEER_PREBUILDS: ${{ runner.temp }}/runtime-host-peer-prebuilds
run: npm run release:cli:pack
- name: Upload the immutable release candidate
id: release-candidate
@@ -77,29 +250,35 @@ jobs:
name: Validate installed CLI ${{ matrix.name }}
needs: build
runs-on: ${{ matrix.runner }}
- timeout-minutes: 15
+ timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- - name: Linux x64 / Node 22.19
+ # The two supported Node versions share a machine and a tarball, so
+ # they are two runs of the same smoke rather than two runners.
+ - name: Linux x64 / Node 22.19 and 24
runner: ubuntu-24.04
node: '22.19.0'
+ second_node: '24'
platform: linux
arch: x64
- - name: Linux x64 / Node 24
- runner: ubuntu-24.04
+ - name: Linux arm64 / Node 24
+ runner: ubuntu-24.04-arm
node: '24'
+ second_node: ''
platform: linux
- arch: x64
+ arch: arm64
- name: macOS arm64 / Node 24
runner: macos-15
node: '24'
+ second_node: ''
platform: darwin
arch: arm64
- name: Windows x64 / Node 24
runner: windows-2025
node: '24'
+ second_node: ''
platform: win32
arch: x64
steps:
@@ -125,12 +304,141 @@ jobs:
path: packages/cli/release
- name: Validate the installed tarball
run: node scripts/smoke-release-cli-package.mjs
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ if: matrix.second_node != ''
+ with:
+ node-version: ${{ matrix.second_node }}
+ - name: Select the release npm toolchain for the second Node
+ if: matrix.second_node != ''
+ run: npm install --global --no-audit --no-fund "$(node -p 'require("./package.json").packageManager')"
+ - name: Validate the installed tarball on the second Node
+ if: matrix.second_node != ''
+ run: node scripts/smoke-release-cli-package.mjs
+
+ state-root-qualification:
+ name: Qualify released State Roots
+ needs: build
+ runs-on: ubuntu-24.04
+ timeout-minutes: 45
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ inputs.source_commit || github.sha }}
+ persist-credentials: false
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: '24'
+ - name: Select the release npm toolchain
+ run: npm install --global --no-audit --no-fund "$(node -p 'require("./package.json").packageManager')"
+ - name: Require the account-isolation sandbox
+ run: |
+ sudo apt-get update
+ sudo apt-get install --yes bubblewrap
+ bwrap --version
+ - name: Download the release candidate
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ artifact-ids: ${{ needs.build.outputs.release_candidate_artifact_id }}
+ path: packages/cli/release
+ # Three runs of one script against one sandbox, not three runners. Two of
+ # these transitions are between tarballs that were published and frozen,
+ # so nothing in a pull request can change their outcome except the
+ # qualifier itself, and the third reads the candidate this run built.
+ - name: Qualify the released State Root transitions
+ env:
+ MAKA_QUALIFICATION_BWRAP_USE_SUDO: '1'
+ PREDECESSOR_TARBALL_URL: ${{ needs.build.outputs.release_predecessor_tarball_url }}
+ PREDECESSOR_INTEGRITY: ${{ needs.build.outputs.release_predecessor_integrity }}
+ run: |
+ set -euo pipefail
+ evidence_root="$RUNNER_TEMP/released-state-root"
+ mkdir -p "$evidence_root"
+
+ qualify() {
+ local slug="$1" source_url="$2" source_sha256="$3" source_integrity="$4"
+ local target_kind="$5" target_url="$6" target_sha256="$7" epoch_relation="$8"
+ local source_path target_path
+ echo "::group::Qualify $slug"
+ source_path="$evidence_root/$slug-source.tgz"
+ curl --fail --location --max-filesize 67108864 --proto '=https' --tlsv1.2 "$source_url" --output "$source_path"
+ if [[ -n "$source_integrity" ]]; then
+ node - "$source_path" "$source_integrity" <<'NODE'
+ const { createHash } = require('node:crypto');
+ const { readFileSync } = require('node:fs');
+ const bytes = readFileSync(process.argv[2]);
+ const actual = `sha512-${createHash('sha512').update(bytes).digest('base64')}`;
+ if (actual !== process.argv[3]) throw new Error('Source tarball integrity mismatch');
+ NODE
+ source_sha256="$(sha256sum "$source_path" | cut -d ' ' -f 1)"
+ else
+ test -n "$source_sha256"
+ fi
+ if [[ "$target_kind" == 'published' ]]; then
+ target_path="$evidence_root/$slug-target.tgz"
+ curl --fail --location --max-filesize 67108864 --proto '=https' --tlsv1.2 "$target_url" --output "$target_path"
+ test -n "$target_sha256"
+ else
+ target_path="$(find packages/cli/release -maxdepth 1 -name '*.tgz' -print -quit)"
+ test -n "$target_path"
+ target_path="$(realpath "$target_path")"
+ target_sha256="$(sha256sum "$target_path" | cut -d ' ' -f 1)"
+ fi
+ npm run --silent release:cli:qualify-state-root -- \
+ --source "$source_path" \
+ --source-sha256 "$source_sha256" \
+ --target "$target_path" \
+ --target-sha256 "$target_sha256" \
+ --expect-epoch-relation "$epoch_relation" \
+ | tee "$evidence_root/$slug-report.json"
+ echo "::endgroup::"
+ }
+
+ qualify cross-epoch-74-to-76 \
+ https://registry.npmjs.org/maka-agent/-/maka-agent-0.2.0-dev.3.20260830.tgz \
+ 66b1ce9307c9d5c06eaa7a6cbf533d4747d02caf71c1776c69c7dbfa12c3f414 '' \
+ published https://registry.npmjs.org/maka-agent/-/maka-agent-0.2.0-dev.4.20260830.tgz \
+ b7d48adb466e16be7ffefbda3a0fcd833cc4108ea502b27778d0f4da680e1fc0 different
+
+ qualify same-epoch-76 \
+ https://registry.npmjs.org/maka-agent/-/maka-agent-0.2.0-dev.4.20260830.tgz \
+ b7d48adb466e16be7ffefbda3a0fcd833cc4108ea502b27778d0f4da680e1fc0 '' \
+ published https://registry.npmjs.org/maka-agent/-/maka-agent-0.2.0-dev.5.20260830.tgz \
+ e7a682157c6899fc7f1be86a2d7b0bd0696195a5771d8cc97bd1389a5b74989f same
+
+ qualify current-nightly-predecessor-to-candidate \
+ "$PREDECESSOR_TARBALL_URL" '' "$PREDECESSOR_INTEGRITY" \
+ candidate '' '' any
+ # The three transitions used to be three matrix jobs, so one failing left
+ # the others to upload their own reports. Folded into one step they share
+ # a `set -e`, and the reports are wanted most on the run that failed —
+ # `tee` has already written the failing transition's own output by then.
+ # `if-no-files-found` stays `error` so a broken path is still caught on a
+ # green run.
+ - name: Preserve the qualification reports
+ if: always()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: released-state-root
+ path: ${{ runner.temp }}/released-state-root/*-report.json
+ if-no-files-found: error
+ retention-days: 7
+ - name: Require the qualified Nightly predecessor to remain current
+ env:
+ PREDECESSOR_VERSION: ${{ needs.build.outputs.release_predecessor_version }}
+ PREDECESSOR_TARBALL_URL: ${{ needs.build.outputs.release_predecessor_tarball_url }}
+ PREDECESSOR_INTEGRITY: ${{ needs.build.outputs.release_predecessor_integrity }}
+ run: |
+ node scripts/release-cli-publication.mjs assert-nightly-predecessor \
+ "$PREDECESSOR_VERSION" \
+ "$PREDECESSOR_TARBALL_URL" \
+ "$PREDECESSOR_INTEGRITY"
eval:
name: Validate installed CLI Eval
+ if: github.event_name != 'pull_request'
needs: build
runs-on: ubuntu-24.04
- timeout-minutes: 30
+ timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
diff --git a/.github/workflows/copilot-auto-review.yml b/.github/workflows/copilot-auto-review.yml
index 858a9a08c8..a33f7e1de6 100644
--- a/.github/workflows/copilot-auto-review.yml
+++ b/.github/workflows/copilot-auto-review.yml
@@ -33,7 +33,7 @@ jobs:
github.event.pull_request.draft == false &&
!endsWith(github.event.pull_request.user.login, '[bot]') &&
!contains(github.event.pull_request.labels.*.name, 'copilot-skip')
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml
index b1b4540837..9219469ff9 100644
--- a/.github/workflows/dependency-audit.yml
+++ b/.github/workflows/dependency-audit.yml
@@ -34,6 +34,7 @@ on:
push:
branches: [main]
paths:
+ - .github/workflows/dependency-audit.yml
- scripts/audit-shipped-dependencies.mjs
- scripts/third-party-closure.mjs
- package.json
@@ -50,8 +51,8 @@ concurrency:
jobs:
audit:
- runs-on: ubuntu-latest
- timeout-minutes: 15
+ runs-on: ubuntu-24.04
+ timeout-minutes: 45
steps:
- name: Check out the repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
diff --git a/.github/workflows/desktop-nightly.yml b/.github/workflows/desktop-nightly.yml
new file mode 100644
index 0000000000..fb03a3b485
--- /dev/null
+++ b/.github/workflows/desktop-nightly.yml
@@ -0,0 +1,322 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: Desktop Nightly
+
+on:
+ workflow_run:
+ workflows:
+ - npm publication
+ types:
+ - completed
+
+permissions:
+ actions: read
+ contents: read
+
+concurrency:
+ group: desktop-nightly
+ cancel-in-progress: false
+
+jobs:
+ identity:
+ if: >-
+ vars.DESKTOP_NIGHTLY_ENABLED == 'true' &&
+ github.event.workflow_run.conclusion == 'success' &&
+ github.event.workflow_run.head_branch == 'main' &&
+ github.event.workflow_run.display_title == 'npm nightly publication'
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
+ outputs:
+ source_commit: ${{ github.event.workflow_run.head_sha }}
+ version: ${{ steps.identity.outputs.version }}
+ steps:
+ - name: Reject in-place workflow reruns
+ if: github.run_attempt != 1
+ run: |
+ echo "Desktop Nightly retries require a fresh npm Nightly dispatch" >&2
+ exit 1
+
+ - name: Check out the published Nightly source
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ github.event.workflow_run.head_sha }}
+ persist-credentials: false
+
+ - name: Require the successful Apache npm Nightly run
+ env:
+ UPSTREAM_REPOSITORY: ${{ github.event.workflow_run.head_repository.full_name }}
+ run: |
+ test "$GITHUB_REPOSITORY" = apache/maka
+ test "$UPSTREAM_REPOSITORY" = apache/maka
+
+ - name: Download the published Nightly version
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: product-nightly-version
+ path: ${{ runner.temp }}/product-nightly-version
+ github-token: ${{ github.token }}
+ repository: ${{ github.repository }}
+ run-id: ${{ github.event.workflow_run.id }}
+
+ - name: Bind Desktop to the exact npm Nightly version
+ id: identity
+ env:
+ VERSION_PATH: ${{ runner.temp }}/product-nightly-version/version.txt
+ run: |
+ node scripts/product-nightly.mjs inspect-version "$VERSION_PATH" "$GITHUB_OUTPUT"
+
+ desktop:
+ needs: identity
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - platform: macos
+ runner: macos-15
+ - platform: windows
+ runner: windows-2025
+ runs-on: ${{ matrix.runner }}
+ environment: nightly
+ timeout-minutes: 75
+ defaults:
+ run:
+ shell: bash
+ env:
+ MAKA_DESKTOP_NIGHTLY_VERSION: ${{ needs.identity.outputs.version }}
+ steps:
+ - name: Reject in-place workflow reruns
+ if: github.run_attempt != 1
+ run: |
+ echo "Desktop Nightly retries require a fresh npm Nightly dispatch" >&2
+ exit 1
+
+ - name: Check out the exact Nightly source
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ needs.identity.outputs.source_commit }}
+ persist-credentials: false
+
+ - name: Set up Node.js
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: '24'
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Audit the shipped Desktop dependency closure
+ run: |
+ npm audit --omit=dev --audit-level=moderate
+ node scripts/audit-shipped-dependencies.mjs
+
+ - name: Update stable Rust for native Desktop artifacts
+ run: rustup update stable --no-self-update
+
+ - name: Write the App Store Connect API key
+ if: matrix.platform == 'macos'
+ env:
+ APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY }}
+ APPLE_API_KEY_PATH: ${{ runner.temp }}/AuthKey_Maka.p8
+ run: |
+ umask 077
+ printf '%s' "$APPLE_API_KEY_CONTENT" > "$APPLE_API_KEY_PATH"
+
+ - name: Package the signed and notarized macOS Nightly
+ if: matrix.platform == 'macos'
+ env:
+ CSC_LINK: ${{ secrets.CSC_LINK }}
+ CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
+ APPLE_API_KEY: ${{ runner.temp }}/AuthKey_Maka.p8
+ APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
+ APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
+ run: npm run package:macos-arm64
+
+ - name: Notarize and verify the macOS Nightly
+ if: matrix.platform == 'macos'
+ env:
+ APPLE_API_KEY: ${{ runner.temp }}/AuthKey_Maka.p8
+ APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
+ APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
+ DMG_PATH: apps/desktop/release/Maka-${{ needs.identity.outputs.version }}-mac-arm64.dmg
+ run: |
+ xcrun notarytool submit "$DMG_PATH" \
+ --key "$APPLE_API_KEY" \
+ --key-id "$APPLE_API_KEY_ID" \
+ --issuer "$APPLE_API_ISSUER" \
+ --wait
+ xcrun stapler staple "$DMG_PATH"
+ npm run verify:macos-arm64 -- "$DMG_PATH"
+
+ - name: Package the Windows Nightly
+ if: matrix.platform == 'windows'
+ run: npm run package:windows-x64
+
+ - name: Verify the Windows Nightly
+ if: matrix.platform == 'windows'
+ run: >-
+ npm run verify:windows-x64 --
+ "apps/desktop/release/Maka-${{ needs.identity.outputs.version }}-win-x64.exe"
+
+ - name: Stage the exact Nightly artifacts
+ env:
+ NIGHTLY_VERSION: ${{ needs.identity.outputs.version }}
+ STAGE_DIRECTORY: ${{ runner.temp }}/desktop-nightly
+ run: |
+ mkdir -p "$STAGE_DIRECTORY"
+ if [[ "${{ matrix.platform }}" = macos ]]; then
+ cp -- \
+ "apps/desktop/release/Maka-$NIGHTLY_VERSION-mac-arm64.dmg" \
+ "apps/desktop/release/Maka-$NIGHTLY_VERSION-mac-arm64.zip" \
+ "apps/desktop/release/Maka-$NIGHTLY_VERSION-mac-arm64.zip.blockmap" \
+ apps/desktop/release/dev-mac.yml \
+ "$STAGE_DIRECTORY/"
+ else
+ cp -- \
+ "apps/desktop/release/Maka-$NIGHTLY_VERSION-win-x64.exe" \
+ "apps/desktop/release/Maka-$NIGHTLY_VERSION-win-x64.exe.blockmap" \
+ "apps/desktop/release/Maka-$NIGHTLY_VERSION-win-x64.zip" \
+ apps/desktop/release/dev.yml \
+ "$STAGE_DIRECTORY/"
+ fi
+
+ - name: Preserve the verified Nightly artifacts
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: desktop-nightly-${{ matrix.platform }}
+ path: ${{ runner.temp }}/desktop-nightly
+ if-no-files-found: error
+ compression-level: 0
+ retention-days: 30
+
+ publish:
+ needs: [identity, desktop]
+ runs-on: ubuntu-24.04
+ environment: nightly
+ timeout-minutes: 20
+ permissions:
+ artifact-metadata: write
+ attestations: write
+ contents: write
+ id-token: write
+ steps:
+ - name: Reject in-place workflow reruns
+ if: github.run_attempt != 1
+ run: |
+ echo "Desktop Nightly retries require a fresh npm Nightly dispatch" >&2
+ exit 1
+
+ - name: Check out the Nightly publisher
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ needs.identity.outputs.source_commit }}
+ persist-credentials: false
+
+ - name: Set up Node.js
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: '24'
+ cache: npm
+
+ - name: Install publisher dependencies
+ run: npm ci --ignore-scripts
+
+ - name: Download both verified Desktop builds
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ pattern: desktop-nightly-*
+ path: ${{ github.workspace }}/.nightly-input
+ merge-multiple: true
+
+ - name: Stage the exact GitHub Release assets
+ env:
+ NIGHTLY_VERSION: ${{ needs.identity.outputs.version }}
+ run: |
+ node scripts/desktop-nightly.mjs stage \
+ "$GITHUB_WORKSPACE/.nightly-input" \
+ "$GITHUB_WORKSPACE/.nightly-stage" \
+ "$NIGHTLY_VERSION"
+
+ - name: Attest every GitHub Nightly asset subject
+ id: attest
+ uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
+ with:
+ subject-path: ${{ github.workspace }}/.nightly-stage/release/*
+
+ - name: Verify the issued Nightly provenance
+ env:
+ ATTESTATION_BUNDLE: ${{ steps.attest.outputs.bundle-path }}
+ CERTIFICATE_IDENTITY: https://github.com/${{ github.repository }}/.github/workflows/desktop-nightly.yml@refs/heads/main
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ verified=0
+ while IFS= read -r -d '' artifact; do
+ gh attestation verify "$artifact" \
+ --bundle "$ATTESTATION_BUNDLE" \
+ --repo "$GITHUB_REPOSITORY" \
+ --cert-identity "$CERTIFICATE_IDENTITY" \
+ --cert-oidc-issuer https://token.actions.githubusercontent.com
+ verified=$((verified + 1))
+ done < <(find "$GITHUB_WORKSPACE/.nightly-stage/release" -maxdepth 1 -type f -print0)
+ if (( verified != 8 )); then
+ echo "Expected 8 verified Desktop Nightly subjects, found $verified" >&2
+ exit 1
+ fi
+
+ - name: Add the one offline provenance bundle
+ env:
+ ATTESTATION_BUNDLE: ${{ steps.attest.outputs.bundle-path }}
+ NIGHTLY_VERSION: ${{ needs.identity.outputs.version }}
+ run: |
+ node scripts/desktop-nightly.mjs add-attestation \
+ "$GITHUB_WORKSPACE/.nightly-stage" \
+ "$NIGHTLY_VERSION" \
+ "$ATTESTATION_BUNDLE"
+
+ - name: Ensure the exact versioned Nightly tag
+ env:
+ GH_TOKEN: ${{ github.token }}
+ NIGHTLY_VERSION: ${{ needs.identity.outputs.version }}
+ SOURCE_COMMIT: ${{ needs.identity.outputs.source_commit }}
+ run: |
+ gh auth setup-git
+ node scripts/product-release-tag.mjs ensure "v$NIGHTLY_VERSION" "$SOURCE_COMMIT"
+
+ - name: Prepare and verify the draft GitHub Prerelease
+ env:
+ GH_TOKEN: ${{ github.token }}
+ NIGHTLY_VERSION: ${{ needs.identity.outputs.version }}
+ SOURCE_COMMIT: ${{ needs.identity.outputs.source_commit }}
+ run: |
+ node scripts/desktop-nightly-release.mjs prepare \
+ "$GITHUB_WORKSPACE/.nightly-stage/release" \
+ "$NIGHTLY_VERSION" \
+ "$SOURCE_COMMIT" \
+ "$GITHUB_REPOSITORY"
+
+ - name: Publish the complete GitHub Prerelease
+ env:
+ GH_TOKEN: ${{ github.token }}
+ NIGHTLY_VERSION: ${{ needs.identity.outputs.version }}
+ SOURCE_COMMIT: ${{ needs.identity.outputs.source_commit }}
+ run: |
+ node scripts/desktop-nightly-release.mjs publish \
+ "$GITHUB_WORKSPACE/.nightly-stage/release" \
+ "$NIGHTLY_VERSION" \
+ "$SOURCE_COMMIT" \
+ "$GITHUB_REPOSITORY"
diff --git a/.github/workflows/gitoxide-helper-admission.yml b/.github/workflows/gitoxide-helper-admission.yml
new file mode 100644
index 0000000000..f1b35bcd75
--- /dev/null
+++ b/.github/workflows/gitoxide-helper-admission.yml
@@ -0,0 +1,122 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: Gitoxide helper admission
+
+on:
+ pull_request:
+ paths:
+ - '.github/workflows/gitoxide-helper-admission.yml'
+ - 'native/gitoxide-helper/**'
+ - 'packages/runtime-host/src/server/gitoxide-helper-*.ts'
+ - 'packages/runtime-host/src/__tests__/gitoxide-helper-*.test.ts'
+ - 'packages/runtime-host/src/server/gitoxide-repository-admission-authority-internal.ts'
+ - 'packages/runtime-host/src/__tests__/gitoxide-repository-admission-authority-internal.test.ts'
+ - 'docs/architecture/gitoxide-*.md'
+ push:
+ branches:
+ - main
+ paths:
+ - '.github/workflows/gitoxide-helper-admission.yml'
+ - 'native/gitoxide-helper/**'
+ - 'packages/runtime-host/src/server/gitoxide-helper-*.ts'
+ - 'packages/runtime-host/src/__tests__/gitoxide-helper-*.test.ts'
+ - 'packages/runtime-host/src/server/gitoxide-repository-admission-authority-internal.ts'
+ - 'packages/runtime-host/src/__tests__/gitoxide-repository-admission-authority-internal.test.ts'
+ - 'docs/architecture/gitoxide-*.md'
+
+permissions:
+ contents: read
+
+concurrency:
+ group: gitoxide-helper-admission-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ test:
+ name: ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 45
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-24.04
+ - macos-latest
+ - windows-latest
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: 24.18.1
+ cache: npm
+ - id: rustc
+ name: Resolve Rust cache version
+ shell: bash
+ run: |
+ echo "version=$(rustc --version | cut -d ' ' -f 2)" >> "$GITHUB_OUTPUT"
+ echo "revision=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
+ {
+ echo "KACHE_CACHE_DIR=${{ runner.temp }}/kache"
+ echo "KACHE_RUNTIME_DIR=${{ runner.temp }}/kache-runtime"
+ echo "RUSTC_WRAPPER=kache"
+ } >> "$GITHUB_ENV"
+ - name: Install Kache
+ uses: taiki-e/install-action@1ed6d7be6168f6c9046541087ff549b6bc581fdf # v2
+ with:
+ tool: kache@0.16.0
+ - id: kache-cache
+ name: Restore Rust build cache
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: ${{ runner.temp }}/kache
+ key: kache-gitoxide-helper-test-v0.16.0-${{ runner.os }}-${{ runner.arch }}-rust-${{ steps.rustc.outputs.version }}-${{ steps.rustc.outputs.revision }}
+ restore-keys: |
+ kache-gitoxide-helper-test-v0.16.0-${{ runner.os }}-${{ runner.arch }}-rust-${{ steps.rustc.outputs.version }}-
+ - name: Check Rust formatting
+ working-directory: native/gitoxide-helper
+ run: cargo fmt --check
+ - name: Test the short-lived Gitoxide helper
+ working-directory: native/gitoxide-helper
+ run: cargo test --locked
+ - name: Report Rust build cache
+ shell: bash
+ run: kache report --format github >> "$GITHUB_STEP_SUMMARY"
+ - name: Save Rust build cache
+ if: github.ref_name == github.event.repository.default_branch
+ uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: ${{ runner.temp }}/kache
+ key: ${{ steps.kache-cache.outputs.cache-primary-key }}
+ - name: Install JavaScript dependencies without packaging hooks
+ run: npm ci --ignore-scripts
+ - name: Build the helper invocation owner
+ run: >-
+ npm --workspace @maka/core run build &&
+ npm --workspace @maka/storage run build &&
+ npm --workspace @maka/runtime run build &&
+ npm --workspace @maka/runtime-host run build
+ - name: Test the real helper invocation contract
+ env:
+ MAKA_GITOXIDE_HELPER_PATH: ${{ github.workspace }}/native/gitoxide-helper/target/debug/maka-gitoxide-helper${{ runner.os == 'Windows' && '.exe' || '' }}
+ run: >-
+ node --test
+ packages/runtime-host/dist/__tests__/gitoxide-helper-artifact-authority-internal.test.js
+ packages/runtime-host/dist/__tests__/gitoxide-helper-invocation-internal.test.js
+ packages/runtime-host/dist/__tests__/gitoxide-repository-admission-authority-internal.test.js
diff --git a/.github/workflows/issue-pr-lifecycle.yml b/.github/workflows/issue-pr-lifecycle.yml
new file mode 100644
index 0000000000..9515e03821
--- /dev/null
+++ b/.github/workflows/issue-pr-lifecycle.yml
@@ -0,0 +1,317 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: Issue and PR lifecycle
+
+on:
+ schedule:
+ - cron: "41 3 * * *"
+ workflow_dispatch:
+ inputs:
+ dry_run:
+ description: Report planned lifecycle changes without writing to GitHub.
+ required: false
+ default: true
+ type: boolean
+ issues:
+ types: [reopened]
+ # Reopen handling mutates labels/comments. Use the trusted base workflow so
+ # fork PRs cannot force writes through a read-only pull_request token.
+ pull_request_target:
+ types: [reopened]
+
+permissions:
+ contents: read
+ issues: write
+ pull-requests: write
+
+concurrency:
+ group: issue-pr-lifecycle
+ cancel-in-progress: false
+
+jobs:
+ lifecycle:
+ if: github.repository == 'apache/maka'
+ runs-on: ubuntu-24.04
+ timeout-minutes: 15
+
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ github.sha }}
+ persist-credentials: false
+
+ - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false }}
+ with:
+ script: |
+ const path = require("node:path")
+ const { pathToFileURL } = require("node:url")
+
+ const {
+ LIFECYCLE_LABELS,
+ STALE_CLOSE_MARKER,
+ planLifecycle,
+ } = await import(
+ pathToFileURL(
+ path.join(process.env.GITHUB_WORKSPACE, "scripts/issue-pr-lifecycle.mjs"),
+ ).href
+ )
+
+ const { owner, repo } = context.repo
+ const dryRun = process.env.DRY_RUN === "true"
+
+ const fragments = `
+ fragment IssueFields on Issue {
+ number
+ state
+ createdAt
+ labels(first: 100) { nodes { name } }
+ assignees(first: 1) { totalCount }
+ comments(last: 100) {
+ totalCount
+ nodes { createdAt body author { __typename login } }
+ }
+ }
+
+ fragment PullRequestFields on PullRequest {
+ number
+ state
+ createdAt
+ labels(first: 100) { nodes { name } }
+ comments(last: 100) {
+ totalCount
+ nodes { createdAt body author { __typename login } }
+ }
+ commits(last: 1) { nodes { commit { committedDate } } }
+ }
+ `
+
+ const normalize = (node) => ({
+ number: node.number,
+ state: node.state,
+ kind: node.__typename === "Issue" ? "issue" : "pull_request",
+ createdAt: node.createdAt,
+ lastCommitAt: node.commits?.nodes?.[0]?.commit?.committedDate,
+ labels: (node.labels?.nodes ?? []).map((label) => label.name),
+ assigneeCount: node.assignees?.totalCount ?? 0,
+ comments: node.comments?.nodes ?? [],
+ commentCount: node.comments?.totalCount ?? node.comments?.nodes?.length ?? 0,
+ })
+
+ async function hydrateComments(item) {
+ if (item.commentCount <= item.comments.length) return item
+ const comments = await github.paginate(github.rest.issues.listComments, {
+ owner,
+ repo,
+ issue_number: item.number,
+ per_page: 100,
+ })
+ return {
+ ...item,
+ comments: comments.map((comment) => ({
+ createdAt: comment.created_at,
+ body: comment.body,
+ author: {
+ __typename: comment.user?.type === "Bot" ? "Bot" : "User",
+ login: comment.user?.login,
+ },
+ })),
+ commentCount: comments.length,
+ }
+ }
+
+ async function listOpenItems() {
+ const query = `
+ query LifecycleItems($searchQuery: String!, $cursor: String) {
+ search(query: $searchQuery, type: ISSUE, first: 100, after: $cursor) {
+ nodes {
+ __typename
+ ...IssueFields
+ ...PullRequestFields
+ }
+ pageInfo { hasNextPage endCursor }
+ }
+ }
+ ${fragments}
+ `
+ const items = []
+ let cursor
+
+ do {
+ const result = await github.graphql(query, {
+ searchQuery: `repo:${owner}/${repo} is:open`,
+ cursor,
+ })
+ items.push(...result.search.nodes.map(normalize))
+ cursor = result.search.pageInfo.hasNextPage
+ ? result.search.pageInfo.endCursor
+ : undefined
+ } while (cursor)
+
+ return items
+ }
+
+ async function refresh(number) {
+ const query = `
+ query LifecycleItem($owner: String!, $repo: String!, $number: Int!) {
+ repository(owner: $owner, name: $repo) {
+ issueOrPullRequest(number: $number) {
+ __typename
+ ...IssueFields
+ ...PullRequestFields
+ }
+ }
+ }
+ ${fragments}
+ `
+ const result = await github.graphql(query, { owner, repo, number })
+ const node = result.repository.issueOrPullRequest
+ return node ? normalize(node) : undefined
+ }
+
+ async function ensureLabels() {
+ for (const label of LIFECYCLE_LABELS) {
+ try {
+ await github.rest.issues.getLabel({ owner, repo, name: label.name })
+ } catch (error) {
+ if (error.status !== 404) throw error
+ if (dryRun) {
+ core.info(`[dry-run] create label ${label.name}`)
+ continue
+ }
+ await github.rest.issues.createLabel({ owner, repo, ...label })
+ core.info(`created label ${label.name}`)
+ }
+ }
+ }
+
+ async function removeStale(number) {
+ try {
+ await github.rest.issues.removeLabel({
+ owner,
+ repo,
+ issue_number: number,
+ name: "stale",
+ })
+ } catch (error) {
+ if (error.status !== 404) throw error
+ }
+ }
+
+ async function apply(item, plan, revalidateClose = true) {
+ const prefix = `${item.kind === "issue" ? "issue" : "PR"} #${item.number}`
+ core.info(`${dryRun ? "[dry-run] " : ""}${prefix}: ${plan.action} (${plan.reason ?? "policy threshold"})`)
+ if (dryRun || plan.action === "none") return
+
+ if (plan.action === "warn") {
+ if (!item.labels.includes("stale")) {
+ await github.rest.issues.addLabels({
+ owner,
+ repo,
+ issue_number: item.number,
+ labels: ["stale"],
+ })
+ }
+ await github.rest.issues.createComment({
+ owner,
+ repo,
+ issue_number: item.number,
+ body: plan.message,
+ })
+ return
+ }
+
+ if (plan.action === "unstale") {
+ await removeStale(item.number)
+ return
+ }
+
+ if (plan.action === "close" && revalidateClose) {
+ let current = await refresh(item.number)
+ if (!current || current.state !== "OPEN") {
+ core.info(`${prefix}: close cancelled because the item is no longer open`)
+ return
+ }
+ current = await hydrateComments(current)
+ const currentPlan = planLifecycle(current)
+ if (currentPlan.action !== "close") {
+ core.info(`${prefix}: close cancelled after revalidation`)
+ await apply(current, currentPlan, false)
+ return
+ }
+ item = current
+ plan = currentPlan
+ }
+
+ const alreadyExplained = item.comments.some(
+ (comment) =>
+ comment.author?.__typename === "Bot" &&
+ comment.body.includes(STALE_CLOSE_MARKER),
+ )
+ if (!alreadyExplained) {
+ await github.rest.issues.createComment({
+ owner,
+ repo,
+ issue_number: item.number,
+ body: plan.message,
+ })
+ }
+
+ if (item.kind === "issue") {
+ await github.rest.issues.update({
+ owner,
+ repo,
+ issue_number: item.number,
+ state: "closed",
+ })
+ } else {
+ await github.rest.pulls.update({
+ owner,
+ repo,
+ pull_number: item.number,
+ state: "closed",
+ })
+ }
+ }
+
+ await ensureLabels()
+
+ if (context.payload.action === "reopened") {
+ const number = context.payload.issue?.number ?? context.payload.pull_request?.number
+ if (number !== undefined) {
+ core.info(`${dryRun ? "[dry-run] " : ""}reopened #${number}: remove stale and reset lifecycle clock`)
+ if (!dryRun) {
+ await removeStale(number)
+ await github.rest.issues.createComment({
+ owner,
+ repo,
+ issue_number: number,
+ body: "",
+ })
+ }
+ return
+ }
+ }
+
+ const items = await listOpenItems()
+ core.info(`evaluating ${items.length} open issues and pull requests`)
+ for (const item of items) {
+ const hydrated = await hydrateComments(item)
+ await apply(hydrated, planLifecycle(hydrated))
+ }
diff --git a/.github/workflows/model-metadata-upkeep.yml b/.github/workflows/model-metadata-upkeep.yml
new file mode 100644
index 0000000000..804ae06203
--- /dev/null
+++ b/.github/workflows/model-metadata-upkeep.yml
@@ -0,0 +1,163 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: Model metadata upkeep
+
+on:
+ schedule:
+ # Weekly, offset from the hour to reduce peak-time scheduling delays. The
+ # snapshot is a build input a human reviews, so a nightly cadence would
+ # only stack five near-identical pull requests against one week of upstream
+ # movement, on runners the whole foundation shares.
+ - cron: '41 6 * * 1'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: model-metadata-upkeep
+ cancel-in-progress: false
+
+jobs:
+ refresh:
+ # A fork inherits the schedule but owns neither the branch this pushes nor
+ # the pull request it opens.
+ if: github.repository == 'apache/maka'
+ runs-on: ubuntu-24.04
+ timeout-minutes: 30
+ permissions:
+ contents: write
+ pull-requests: write
+ steps:
+ - name: Check out the repository
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+
+ - name: Set up Node.js
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: '24'
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci --ignore-scripts
+
+ - name: Report snapshot drift against models.dev
+ # Exit 2 is "upstream moved", which is the expected outcome and the
+ # reason this step exists. Any other non-zero status is the command
+ # itself failing, and the job stops on it.
+ run: |
+ npm run --silent check:model-metadata-drift > "$RUNNER_TEMP/drift.txt" || {
+ status=$?
+ cat "$RUNNER_TEMP/drift.txt"
+ [ "$status" -eq 2 ] || exit "$status"
+ }
+
+ - name: Publish the drift report
+ run: |
+ {
+ echo '### models.dev drift'
+ echo ''
+ echo '```text'
+ cat "$RUNNER_TEMP/drift.txt"
+ echo '```'
+ } >> "$GITHUB_STEP_SUMMARY"
+
+ # --accept-upstream-removals, because the review seat this job is built
+ # around is the draft pull request below. A person still inspects every
+ # removal and still decides, in the diff, with the drift report in the
+ # body; refusing here would only make the job red every week, since it
+ # cannot rerun itself the way the acknowledgement asks a human to.
+ - name: Refresh the snapshot from models.dev
+ run: npm run refresh:model-metadata -- --accept-upstream-removals
+
+ - name: Verify the regenerated outputs
+ run: npm run check:model-metadata
+
+ - name: Detect a snapshot change
+ id: change
+ run: |
+ if git diff --quiet -- scripts/model-metadata/models-dev-api.snapshot.json; then
+ echo 'changed=false' >> "$GITHUB_OUTPUT"
+ else
+ echo 'changed=true' >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Open the review pull request
+ if: steps.change.outputs.changed == 'true'
+ env:
+ BRANCH: automation/model-metadata-refresh
+ GH_TOKEN: ${{ github.token }}
+ TITLE: 'chore(model-metadata): refresh the models.dev snapshot'
+ run: |
+ {
+ echo '## Summary'
+ echo ''
+ echo 'Scheduled `refresh:model-metadata` run. The snapshot is the build'
+ echo 'input for the bundled model catalog; this only moves it to what'
+ echo 'models.dev serves today.'
+ echo ''
+ echo 'Refs #4398'
+ echo ''
+ echo '## Verification'
+ echo ''
+ echo 'The workflow ran `refresh:model-metadata` and `check:model-metadata`'
+ echo 'before opening this. Drift against upstream at refresh time:'
+ echo ''
+ echo '```text'
+ cat "$RUNNER_TEMP/drift.txt"
+ echo '```'
+ } > "$RUNNER_TEMP/pr-body.md"
+ git config user.name 'Apache Maka'
+ git config user.email 'commits@maka.apache.org'
+ git switch -c "$BRANCH"
+ git add scripts/model-metadata/models-dev-api.snapshot.json
+ git commit -m "$TITLE"
+ # The token travels in a header rather than the remote URL, which git
+ # echoes back in its own error messages.
+ AUTH="$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')"
+ REMOTE="https://github.com/${GITHUB_REPOSITORY}"
+ TIP="$(git -c "http.extraheader=Authorization: Basic $AUTH" \
+ ls-remote "$REMOTE" "refs/heads/$BRANCH" | cut -f1)"
+ if [ -n "$TIP" ]; then
+ # This branch only ever carries commits this job wrote. Anything
+ # else is a person working on the open pull request, and a force
+ # push would erase it.
+ git -c "http.extraheader=Authorization: Basic $AUTH" \
+ fetch --depth=1 "$REMOTE" "refs/heads/$BRANCH"
+ if [ "$(git log -1 --format=%s FETCH_HEAD)" != "$TITLE" ]; then
+ echo "::error::$BRANCH carries a commit this workflow did not write; refusing to overwrite it."
+ exit 1
+ fi
+ git -c "http.extraheader=Authorization: Basic $AUTH" push \
+ "--force-with-lease=refs/heads/$BRANCH:$TIP" "$REMOTE" "HEAD:refs/heads/$BRANCH"
+ else
+ git -c "http.extraheader=Authorization: Basic $AUTH" push \
+ "$REMOTE" "HEAD:refs/heads/$BRANCH"
+ fi
+ # A plain existence lookup also succeeds for a closed pull request,
+ # which would leave a maintainer's decision to close one silently
+ # disabling this job forever. Only an open one is one to update.
+ if [ "$(gh pr list --head "$BRANCH" --state open --json number --jq 'length')" -gt 0 ]; then
+ gh pr edit "$BRANCH" --body-file "$RUNNER_TEMP/pr-body.md"
+ echo "Updated the open pull request on $BRANCH."
+ else
+ gh pr create --draft --base main --head "$BRANCH" \
+ --title "$TITLE" --body-file "$RUNNER_TEMP/pr-body.md"
+ fi
diff --git a/.github/workflows/npm-publication.yml b/.github/workflows/npm-publication.yml
new file mode 100644
index 0000000000..1fc32f99a1
--- /dev/null
+++ b/.github/workflows/npm-publication.yml
@@ -0,0 +1,204 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: npm publication
+run-name: npm ${{ inputs.channel || 'nightly' }} publication
+
+on:
+ schedule:
+ - cron: '17 18 * * *'
+ workflow_dispatch:
+ inputs:
+ channel:
+ description: Publication channel
+ required: true
+ type: choice
+ default: nightly
+ options:
+ - nightly
+ - formal
+ version:
+ description: Exact formal product version; ignored for Nightly
+ required: false
+ type: string
+ default: ''
+
+permissions:
+ contents: read
+
+concurrency:
+ group: npm-publication-${{ inputs.channel || 'nightly' }}
+ cancel-in-progress: false
+
+jobs:
+ formal:
+ if: github.event_name == 'workflow_dispatch' && inputs.channel == 'formal'
+ permissions:
+ contents: read
+ id-token: write
+ uses: ./.github/workflows/release-cli-stage.yml
+ with:
+ version: ${{ inputs.version }}
+
+ identity:
+ if: vars.NPM_NIGHTLY_ENABLED == 'true' && (github.event_name == 'schedule' || inputs.channel == 'nightly')
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
+ outputs:
+ source_commit: ${{ steps.identity.outputs.source_commit }}
+ version: ${{ steps.identity.outputs.version }}
+ steps:
+ - name: Reject in-place workflow reruns
+ if: github.run_attempt != 1
+ run: |
+ echo "npm Nightly retries require a fresh workflow dispatch" >&2
+ exit 1
+
+ - name: Check out the scheduled main commit
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ github.sha }}
+ persist-credentials: false
+
+ - name: Require the Apache main branch
+ run: |
+ test "$GITHUB_REPOSITORY" = apache/maka
+ test "$GITHUB_REF" = refs/heads/main
+
+ - name: Resolve the immutable Nightly identity
+ id: identity
+ env:
+ GITHUB_SHA: ${{ github.sha }}
+ GITHUB_RUN_NUMBER: ${{ github.run_number }}
+ run: node scripts/product-nightly.mjs identity
+
+ cli:
+ name: Validate npm Nightly
+ needs: identity
+ uses: ./.github/workflows/cli-package-validation.yml
+ with:
+ source_commit: ${{ needs.identity.outputs.source_commit }}
+ package_version: ${{ needs.identity.outputs.version }}
+
+ publish:
+ name: Publish npm Nightly
+ needs: [identity, cli]
+ runs-on: ubuntu-24.04
+ environment: npm-publication
+ timeout-minutes: 20
+ permissions:
+ contents: read
+ id-token: write
+ steps:
+ - name: Reject in-place workflow reruns
+ if: github.run_attempt != 1
+ run: |
+ echo "npm Nightly retries require a fresh workflow dispatch" >&2
+ exit 1
+
+ - name: Check out the Nightly publisher
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ needs.identity.outputs.source_commit }}
+ persist-credentials: false
+
+ - name: Set up Node.js
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: '24'
+ cache: npm
+
+ - name: Install publisher dependencies
+ run: npm ci --ignore-scripts
+
+ - name: Select the publication npm toolchain
+ run: npm install --global --no-audit --no-fund "$(node -p 'require("./package.json").packageManager')"
+
+ - name: Download the validated npm Nightly
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ artifact-ids: ${{ needs.cli.outputs.release_candidate_artifact_id }}
+ path: packages/cli/release
+
+ - name: Bind the npm Nightly to this workflow run
+ id: npm-nightly
+ env:
+ NIGHTLY_VERSION: ${{ needs.identity.outputs.version }}
+ run: |
+ node scripts/release-cli-publication.mjs prepare-nightly \
+ packages/cli/release \
+ "$NIGHTLY_VERSION" \
+ "$GITHUB_OUTPUT"
+
+ - name: Require the qualified predecessor and Nightly channel advance
+ env:
+ NIGHTLY_VERSION: ${{ steps.npm-nightly.outputs.version }}
+ PREDECESSOR_VERSION: ${{ needs.cli.outputs.release_predecessor_version }}
+ PREDECESSOR_TARBALL_URL: ${{ needs.cli.outputs.release_predecessor_tarball_url }}
+ PREDECESSOR_INTEGRITY: ${{ needs.cli.outputs.release_predecessor_integrity }}
+ run: |
+ node scripts/release-cli-publication.mjs assert-nightly-predecessor \
+ "$PREDECESSOR_VERSION" \
+ "$PREDECESSOR_TARBALL_URL" \
+ "$PREDECESSOR_INTEGRITY"
+ current="$(npm view maka-agent dist-tags.nightly --registry https://registry.npmjs.org/)"
+ node scripts/product-nightly.mjs assert-channel-advance "$NIGHTLY_VERSION" "$current"
+
+ - name: Publish the exact npm Nightly
+ env:
+ NIGHTLY_DIST_TAG: ${{ steps.npm-nightly.outputs.dist_tag }}
+ NIGHTLY_TARBALL: ${{ steps.npm-nightly.outputs.tarball }}
+ run: |
+ test "$NIGHTLY_DIST_TAG" = nightly
+ npm publish "$NIGHTLY_TARBALL" \
+ --tag nightly \
+ --registry https://registry.npmjs.org/ \
+ --provenance
+
+ - name: Require the public npm Nightly
+ env:
+ NIGHTLY_VERSION: ${{ steps.npm-nightly.outputs.version }}
+ run: |
+ for attempt in {1..12}; do
+ published="$(npm view "maka-agent@$NIGHTLY_VERSION" version --registry https://registry.npmjs.org/ 2>/dev/null || true)"
+ tagged="$(npm view maka-agent dist-tags.nightly --registry https://registry.npmjs.org/ 2>/dev/null || true)"
+ if [[ "$published" == "$NIGHTLY_VERSION" && "$tagged" == "$NIGHTLY_VERSION" ]]; then
+ exit 0
+ fi
+ sleep 5
+ done
+ echo "npm Nightly did not become readable with the exact nightly tag" >&2
+ exit 1
+
+ - name: Record the published Product Nightly version
+ env:
+ VERSION_DIRECTORY: ${{ runner.temp }}/product-nightly-version
+ NIGHTLY_VERSION: ${{ steps.npm-nightly.outputs.version }}
+ run: |
+ mkdir -p "$VERSION_DIRECTORY"
+ node scripts/product-nightly.mjs write-version \
+ "$VERSION_DIRECTORY/version.txt" \
+ "$NIGHTLY_VERSION"
+
+ - name: Hand the exact version to Desktop Nightly
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: product-nightly-version
+ path: ${{ runner.temp }}/product-nightly-version/version.txt
+ if-no-files-found: error
+ compression-level: 0
+ retention-days: 30
diff --git a/.github/workflows/pr-effort-label.yml b/.github/workflows/pr-effort-label.yml
new file mode 100644
index 0000000000..ac6d32ec8d
--- /dev/null
+++ b/.github/workflows/pr-effort-label.yml
@@ -0,0 +1,134 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: PR effort label
+
+# Labels a pull request when it first asks for review, then re-checks the whole
+# open set once a day. A push changes the diff and so can change the tier, but
+# subscribing to every push would run this hundreds of times a day to correct a
+# label nobody is misled by in the meantime; the daily sweep absorbs that drift
+# and doubles as the recovery path for any event this misses.
+on:
+ pull_request_target:
+ types: [opened, reopened, ready_for_review]
+ branches: [main]
+ schedule:
+ - cron: "23 4 * * *"
+ workflow_dispatch:
+ inputs:
+ dry_run:
+ description: Log the tier each open pull request would get without writing labels.
+ required: false
+ default: false
+ type: boolean
+
+permissions:
+ contents: read
+ pull-requests: write
+
+concurrency:
+ group: pr-effort-label-${{ github.event.pull_request.number || github.workflow }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request_target' }}
+
+jobs:
+ label:
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
+
+ steps:
+ # pull_request_target runs with a writable token, so the ref is pinned to
+ # the trusted default-branch commit rather than left to the event's
+ # default: pull_request_review and friends resolve to refs/pull/N/merge,
+ # and this job imports the checked-out script.
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ github.sha }}
+ persist-credentials: false
+
+ - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ DRY_RUN: ${{ inputs.dry_run || false }}
+ with:
+ script: |
+ const path = require("node:path")
+ const { pathToFileURL } = require("node:url")
+
+ const { planLabels } = await import(
+ pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, "scripts/pr-effort.mjs")).href
+ )
+
+ const { owner, repo } = context.repo
+
+ // Reviewing a tier boundary or an exclusion means seeing it against
+ // real pull requests, which is this same sweep minus the writes.
+ const dryRun = process.env.DRY_RUN === "true"
+
+ const targets = context.payload.pull_request
+ ? [context.payload.pull_request.number]
+ : (
+ await github.paginate(github.rest.pulls.list, {
+ owner,
+ repo,
+ state: "open",
+ per_page: 100,
+ })
+ ).map((pull) => pull.number)
+
+ for (const pull_number of targets) {
+ const [files, current] = await Promise.all([
+ github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 }),
+ github.paginate(github.rest.issues.listLabelsOnIssue, {
+ owner,
+ repo,
+ issue_number: pull_number,
+ per_page: 100,
+ }),
+ ])
+
+ const plan = planLabels(
+ files,
+ current.map((label) => label.name),
+ )
+
+ if (dryRun) {
+ core.info(
+ `#${pull_number}: ${plan.label} (${plan.lines} readable lines)` +
+ ` +[${plan.addLabels.join(", ")}] -[${plan.removeLabels.join(", ")}]`,
+ )
+ continue
+ }
+
+ if (plan.addLabels.length > 0) {
+ await github.rest.issues.addLabels({
+ owner,
+ repo,
+ issue_number: pull_number,
+ labels: plan.addLabels,
+ })
+ }
+
+ for (const name of plan.removeLabels) {
+ try {
+ await github.rest.issues.removeLabel({ owner, repo, issue_number: pull_number, name })
+ } catch (error) {
+ // Another run may have removed it first; anything else is real.
+ if (error.status !== 404) throw error
+ }
+ }
+
+ core.info(`#${pull_number}: ${plan.label} (${plan.lines} readable lines)`)
+ }
diff --git a/.github/workflows/release-cli-finalize.yml b/.github/workflows/release-cli-finalize.yml
index 7909ce03ca..49c7667a1d 100644
--- a/.github/workflows/release-cli-finalize.yml
+++ b/.github/workflows/release-cli-finalize.yml
@@ -15,7 +15,7 @@
# specific language governing permissions and limitations
# under the License.
-name: Finalize CLI npm channel
+name: Finalize product release
on:
workflow_dispatch:
@@ -28,6 +28,14 @@ on:
description: Successful Stage CLI npm release workflow run attempt
required: true
type: string
+ release_run_id:
+ description: Successful Release workflow run ID that built the Draft assets
+ required: true
+ type: string
+ release_run_attempt:
+ description: Successful Release workflow run attempt that built the Draft assets
+ required: true
+ type: string
version:
description: Exact staged maka-agent product version
required: true
@@ -38,7 +46,7 @@ permissions:
contents: read
concurrency:
- group: cli-npm-finalize
+ group: product-release
cancel-in-progress: false
jobs:
@@ -46,13 +54,20 @@ jobs:
name: Verify the public npm channel
runs-on: ubuntu-24.04
timeout-minutes: 20
+ outputs:
+ product_tag: ${{ steps.release.outputs.product_tag }}
+ product_version: ${{ steps.release.outputs.version }}
+ release_run_id: ${{ steps.release-run.outputs.run_id }}
+ release_run_attempt: ${{ steps.release-run.outputs.run_attempt }}
+ source_commit: ${{ steps.release.outputs.source_commit }}
+ source_reference_tag: ${{ steps.authority.outputs.source_reference_tag }}
steps:
- name: Require main
env:
RELEASE_REF: ${{ github.ref }}
run: |
if [[ "$RELEASE_REF" != "refs/heads/main" ]]; then
- echo "CLI npm finalization must be dispatched from main; found $RELEASE_REF" >&2
+ echo "Product finalization must be dispatched from main; found $RELEASE_REF" >&2
exit 1
fi
@@ -72,6 +87,25 @@ jobs:
fi
gh api "repos/$GITHUB_REPOSITORY/actions/runs/$STAGE_RUN_ID/attempts/$STAGE_RUN_ATTEMPT" > "$RUNNER_TEMP/stage-run.json"
+ - name: Load the exact Release workflow run
+ id: release-run
+ env:
+ GH_TOKEN: ${{ github.token }}
+ RELEASE_RUN_ID: ${{ inputs.release_run_id }}
+ RELEASE_RUN_ATTEMPT: ${{ inputs.release_run_attempt }}
+ run: |
+ if [[ ! "$RELEASE_RUN_ID" =~ ^[1-9][0-9]*$ ]]; then
+ echo "Release workflow run ID must be a positive integer" >&2
+ exit 1
+ fi
+ if [[ ! "$RELEASE_RUN_ATTEMPT" =~ ^[1-9][0-9]*$ ]]; then
+ echo "Release workflow run attempt must be a positive integer" >&2
+ exit 1
+ fi
+ gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RELEASE_RUN_ID/attempts/$RELEASE_RUN_ATTEMPT" > "$RUNNER_TEMP/release-run.json"
+ echo "run_id=$RELEASE_RUN_ID" >> "$GITHUB_OUTPUT"
+ echo "run_attempt=$RELEASE_RUN_ATTEMPT" >> "$GITHUB_OUTPUT"
+
- name: Check out the current release verifier
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
@@ -96,6 +130,15 @@ jobs:
repository: ${{ github.repository }}
run-id: ${{ inputs.stage_run_id }}
+ - name: Download the publication record
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: product-release-record-${{ inputs.release_run_attempt }}
+ path: ${{ runner.temp }}/product-release-record
+ github-token: ${{ github.token }}
+ repository: ${{ github.repository }}
+ run-id: ${{ inputs.release_run_id }}
+
- name: Verify the stage run and release record
id: release
env:
@@ -108,13 +151,33 @@ jobs:
"$GITHUB_OUTPUT"
- name: Revalidate the product release authority
+ id: authority
env:
GH_TOKEN: ${{ github.token }}
PRODUCT_SOURCE_COMMIT: ${{ steps.release.outputs.source_commit }}
PRODUCT_TAG: ${{ steps.release.outputs.product_tag }}
+ RELEASE_RUN_ATTEMPT: ${{ steps.release-run.outputs.run_attempt }}
+ RELEASE_RUN_ID: ${{ steps.release-run.outputs.run_id }}
run: |
+ node scripts/product-release-authority.mjs verify-build-run \
+ "$RUNNER_TEMP/release-run.json" \
+ "$PRODUCT_TAG" \
+ "$PRODUCT_SOURCE_COMMIT" \
+ "$GITHUB_REPOSITORY" \
+ "$RELEASE_RUN_ID" \
+ "$RELEASE_RUN_ATTEMPT"
+ source_reference_tag="$(jq -r .head_branch "$RUNNER_TEMP/release-run.json")"
+ node scripts/product-release-artifacts.mjs inspect-record \
+ "$RUNNER_TEMP/product-release-record/product-release.json" \
+ "$GITHUB_REPOSITORY" \
+ "$PRODUCT_TAG" \
+ "$PRODUCT_SOURCE_COMMIT" \
+ "$source_reference_tag" \
+ "$RELEASE_RUN_ID" \
+ "$RELEASE_RUN_ATTEMPT"
node scripts/product-release-authority.mjs verify-draft \
"$PRODUCT_TAG" "$PRODUCT_SOURCE_COMMIT" "$GITHUB_REPOSITORY"
+ echo "source_reference_tag=$source_reference_tag" >> "$GITHUB_OUTPUT"
- name: Fetch and verify the public registry bytes
run: |
@@ -141,3 +204,123 @@ jobs:
if-no-files-found: error
compression-level: 0
retention-days: 30
+
+ publish:
+ name: Publish the attested convenience release
+ needs: inspect
+ runs-on: ubuntu-24.04
+ timeout-minutes: 30
+ environment:
+ name: product-release
+ url: https://github.com/apache/maka/releases/tag/${{ needs.inspect.outputs.product_tag }}
+ permissions:
+ actions: read
+ artifact-metadata: write
+ attestations: write
+ contents: write
+ id-token: write
+ steps:
+ - name: Check out the current release verifier
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ github.sha }}
+ fetch-depth: 0
+ persist-credentials: false
+
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: '24'
+ package-manager-cache: false
+
+ - name: Download the exact verified Release run artifacts
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ pattern: release-*-${{ needs.inspect.outputs.release_run_attempt }}
+ path: ${{ runner.temp }}/product-release
+ merge-multiple: true
+ github-token: ${{ github.token }}
+ repository: ${{ github.repository }}
+ run-id: ${{ needs.inspect.outputs.release_run_id }}
+
+ - name: Download the publication record
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: product-release-record-${{ needs.inspect.outputs.release_run_attempt }}
+ path: ${{ runner.temp }}/product-release-record
+ github-token: ${{ github.token }}
+ repository: ${{ github.repository }}
+ run-id: ${{ needs.inspect.outputs.release_run_id }}
+
+ - name: Verify the exact publication input
+ env:
+ GH_TOKEN: ${{ github.token }}
+ PRODUCT_SOURCE_COMMIT: ${{ needs.inspect.outputs.source_commit }}
+ PRODUCT_SOURCE_REFERENCE_TAG: ${{ needs.inspect.outputs.source_reference_tag }}
+ PRODUCT_TAG: ${{ needs.inspect.outputs.product_tag }}
+ RELEASE_RUN_ATTEMPT: ${{ needs.inspect.outputs.release_run_attempt }}
+ RELEASE_RUN_ID: ${{ needs.inspect.outputs.release_run_id }}
+ run: |
+ node scripts/product-release-authority.mjs verify-publication \
+ "$PRODUCT_TAG" \
+ "$PRODUCT_SOURCE_COMMIT" \
+ "$GITHUB_REPOSITORY" \
+ "$RUNNER_TEMP/product-release" \
+ "$RUNNER_TEMP/product-release-record/product-release.json" \
+ "$PRODUCT_SOURCE_REFERENCE_TAG" \
+ "$RELEASE_RUN_ID" \
+ "$RELEASE_RUN_ATTEMPT"
+
+ - name: Attest the verified convenience artifacts
+ id: attest
+ uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.0.0
+ with:
+ subject-path: ${{ runner.temp }}/product-release/*
+
+ - name: Verify the issued provenance
+ env:
+ ATTESTATION_BUNDLE: ${{ steps.attest.outputs.bundle-path }}
+ CERTIFICATE_IDENTITY: https://github.com/${{ github.repository }}/.github/workflows/release-cli-finalize.yml@refs/heads/main
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ verified=0
+ while IFS= read -r -d '' artifact; do
+ gh attestation verify "$artifact" \
+ --bundle "$ATTESTATION_BUNDLE" \
+ --repo "$GITHUB_REPOSITORY" \
+ --cert-identity "$CERTIFICATE_IDENTITY" \
+ --cert-oidc-issuer https://token.actions.githubusercontent.com
+ verified=$((verified + 1))
+ done < <(find "$RUNNER_TEMP/product-release" -maxdepth 1 -type f -print0)
+ if (( verified == 0 )); then
+ echo "No product release artifacts were verified" >&2
+ exit 1
+ fi
+
+ - name: Name the offline verification bundle
+ env:
+ ATTESTATION_BUNDLE: ${{ steps.attest.outputs.bundle-path }}
+ PRODUCT_VERSION: ${{ needs.inspect.outputs.product_version }}
+ run: >-
+ cp -- "$ATTESTATION_BUNDLE"
+ "$RUNNER_TEMP/Maka-${PRODUCT_VERSION}-attestation.sigstore.json"
+
+ - name: Publish the verified convenience release
+ env:
+ GH_TOKEN: ${{ github.token }}
+ PRODUCT_SOURCE_COMMIT: ${{ needs.inspect.outputs.source_commit }}
+ PRODUCT_SOURCE_REFERENCE_TAG: ${{ needs.inspect.outputs.source_reference_tag }}
+ PRODUCT_TAG: ${{ needs.inspect.outputs.product_tag }}
+ PRODUCT_VERSION: ${{ needs.inspect.outputs.product_version }}
+ RELEASE_RUN_ATTEMPT: ${{ needs.inspect.outputs.release_run_attempt }}
+ RELEASE_RUN_ID: ${{ needs.inspect.outputs.release_run_id }}
+ run: |
+ node scripts/product-release-authority.mjs publish-draft \
+ "$PRODUCT_TAG" \
+ "$PRODUCT_SOURCE_COMMIT" \
+ "$GITHUB_REPOSITORY" \
+ "$RUNNER_TEMP/product-release" \
+ "$RUNNER_TEMP/product-release-record/product-release.json" \
+ "$PRODUCT_SOURCE_REFERENCE_TAG" \
+ "$RELEASE_RUN_ID" \
+ "$RELEASE_RUN_ATTEMPT" \
+ "$RUNNER_TEMP/Maka-${PRODUCT_VERSION}-attestation.sigstore.json"
diff --git a/.github/workflows/release-cli-stage.yml b/.github/workflows/release-cli-stage.yml
index bda8058009..165bdad170 100644
--- a/.github/workflows/release-cli-stage.yml
+++ b/.github/workflows/release-cli-stage.yml
@@ -18,7 +18,7 @@
name: Stage CLI npm release
on:
- workflow_dispatch:
+ workflow_call:
inputs:
version:
description: Exact maka-agent version from packages/cli/package.json
@@ -41,20 +41,28 @@ jobs:
source_commit: ${{ steps.product.outputs.source_commit }}
product_tag: ${{ steps.product.outputs.product_tag }}
steps:
- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - name: Check out the trusted publisher
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
- ref: v${{ inputs.version }}
+ ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- - name: Require the exact product tag
+
+ - name: Require the Apache main publisher
env:
RELEASE_REF: ${{ github.ref }}
- PRODUCT_TAG: v${{ inputs.version }}
+ RELEASE_REPOSITORY: ${{ github.repository }}
run: |
- if [[ "$RELEASE_REF" != "refs/tags/$PRODUCT_TAG" ]]; then
- echo "CLI releases must be dispatched from $PRODUCT_TAG; found $RELEASE_REF" >&2
- exit 1
- fi
+ test "$RELEASE_REPOSITORY" = apache/maka
+ test "$RELEASE_REF" = refs/heads/main
+
+ - name: Check out the approved product source as data
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: v${{ inputs.version }}
+ path: product-source
+ fetch-depth: 1
+ persist-credentials: false
- name: Resolve the exact product release
id: product
@@ -62,8 +70,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
PRODUCT_TAG: v${{ inputs.version }}
run: |
- source_commit="$(git rev-parse HEAD)"
- test "$source_commit" = "$GITHUB_SHA"
+ source_commit="$(git -C product-source rev-parse HEAD)"
node scripts/product-release-authority.mjs verify-draft \
"$PRODUCT_TAG" "$source_commit" "$GITHUB_REPOSITORY"
echo "source_commit=$source_commit" >> "$GITHUB_OUTPUT"
@@ -73,6 +80,7 @@ jobs:
env:
GITHUB_SHA: ${{ steps.product.outputs.source_commit }}
EXPECTED_PRODUCT_VERSION: ${{ inputs.version }}
+ PRODUCT_MANIFEST_ROOT: ${{ github.workspace }}/product-source
run: node scripts/product-release-identity.mjs
validate:
@@ -88,15 +96,16 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 15
environment:
- name: npm-release
+ name: npm-publication
url: https://www.npmjs.com/package/maka-agent
permissions:
contents: read
id-token: write
steps:
- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - name: Check out trusted staging code
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
- ref: ${{ needs.authorize.outputs.source_commit }}
+ ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
@@ -119,14 +128,16 @@ jobs:
RELEASE_REPOSITORY: ${{ github.repository }}
RELEASE_RUN_ID: ${{ github.run_id }}
RELEASE_RUN_ATTEMPT: ${{ github.run_attempt }}
- RELEASE_SHA: ${{ github.sha }}
- RELEASE_WORKFLOW: .github/workflows/release-cli-stage.yml
+ PRODUCT_SOURCE_SHA: ${{ needs.authorize.outputs.source_commit }}
+ PUBLISHER_SHA: ${{ github.sha }}
+ RELEASE_WORKFLOW: .github/workflows/npm-publication.yml
run: |
node scripts/release-cli-publication.mjs prepare-stage \
packages/cli/release \
"$EXPECTED_VERSION" \
"$PRODUCT_TAG" \
- "$RELEASE_SHA" \
+ "$PRODUCT_SOURCE_SHA" \
+ "$PUBLISHER_SHA" \
"$RELEASE_RUN_ID" \
"$RELEASE_RUN_ATTEMPT" \
"$RELEASE_REPOSITORY" \
@@ -155,26 +166,28 @@ jobs:
echo "## maka-agent@$RELEASE_VERSION staging"
echo
echo "After this workflow succeeds, review and approve the staged package with 2FA on npmjs.com."
- echo "After the package becomes public, run **Finalize CLI npm channel** with:"
+ echo "After the package becomes public, run **Finalize product release** with:"
echo
echo "- stage run ID: \`$RELEASE_RUN_ID\`"
echo "- stage run attempt: \`$RELEASE_RUN_ATTEMPT\`"
echo "- version: \`$RELEASE_VERSION\`"
- if [[ "$RELEASE_DIST_TAG" == "latest" ]]; then
- echo
- echo "Before finalizing this stable release, inspect npm dist-tags. If \`next\` is older than \`latest\`, authenticate interactively with npm and run:"
- echo
- echo "\`npm dist-tag add \"maka-agent@$RELEASE_VERSION\" next --registry https://registry.npmjs.org/\`"
- fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Submit the candidate to npm staging
env:
GH_TOKEN: ${{ github.token }}
+ PREDECESSOR_VERSION: ${{ needs.validate.outputs.release_predecessor_version }}
+ PREDECESSOR_TARBALL_URL: ${{ needs.validate.outputs.release_predecessor_tarball_url }}
+ PREDECESSOR_INTEGRITY: ${{ needs.validate.outputs.release_predecessor_integrity }}
PRODUCT_SOURCE_COMMIT: ${{ needs.authorize.outputs.source_commit }}
PRODUCT_TAG: ${{ needs.authorize.outputs.product_tag }}
RELEASE_DIST_TAG: ${{ steps.release.outputs.dist_tag }}
RELEASE_TARBALL: ${{ steps.release.outputs.tarball }}
run: |
+ node scripts/release-cli-publication.mjs assert-nightly-predecessor \
+ "$PREDECESSOR_VERSION" \
+ "$PREDECESSOR_TARBALL_URL" \
+ "$PREDECESSOR_INTEGRITY"
+
node scripts/product-release-authority.mjs verify-draft \
"$PRODUCT_TAG" "$PRODUCT_SOURCE_COMMIT" "$GITHUB_REPOSITORY"
diff --git a/.github/workflows/release-windows-check.yml b/.github/workflows/release-windows-check.yml
index c8d0c3ae1f..2d7b47f811 100644
--- a/.github/workflows/release-windows-check.yml
+++ b/.github/workflows/release-windows-check.yml
@@ -32,8 +32,12 @@ on:
paths:
- 'apps/desktop/electron-builder.config.mjs'
- 'apps/desktop/package.json'
+ - 'native/runtime-host-peer/**'
+ - 'packages/cli/RUNTIME_HOST_PEER_DEPENDENCIES.rust.tsv'
+ - 'packages/cli/RUNTIME_HOST_PEER_THIRD_PARTY_NOTICES.txt'
- 'scripts/package-windows-x64.mjs'
- - 'scripts/verify-windows-sandbox-e2e.mjs'
+ - 'scripts/generate-runtime-host-peer-dependencies.mjs'
+ - 'scripts/generate-runtime-host-peer-notices.mjs'
- 'scripts/verify-windows-x64.mjs'
- 'scripts/verify-windows-sandbox-e2e.mjs'
- 'scripts/verify-windows-installer-lifecycle.mjs'
@@ -42,11 +46,13 @@ on:
- 'scripts/package-windows-autoupdate-next.mjs'
# The Abort-path rollback hook ships inside the installer itself.
- 'apps/desktop/build/installer.nsh'
- # The packaged updater's feed behavior — and the boot wiring that hands
- # MAKA_UPDATE_TEST_FEED to it — is only observable on this path.
+ # The packaged updater's feed behavior is only observable on this path.
+ # The boot wiring that hands MAKA_UPDATE_TEST_FEED to it is not here:
+ # that is a few lines, `scripts/update-test-feed-wiring.test.mjs` asserts
+ # them on every change for free, and naming the file made a 1900-line
+ # module the single largest source of runs on this 25-minute lane.
- 'apps/desktop/src/main/app-update-service.ts'
- 'apps/desktop/src/main/main-window.ts'
- - 'apps/desktop/src/main/runtime-host-boot.ts'
- 'packages/runtime-host/src/client/connect-or-spawn.ts'
- 'packages/runtime-host/src/client/launcher.ts'
- 'apps/desktop/src/main/windows-maximize-renderer-sync.ts'
@@ -56,26 +62,62 @@ on:
- 'scripts/npm-spawn.mjs'
- 'scripts/generate-third-party-notices.mjs'
- 'scripts/generate-windows-cargo-notices.mjs'
+ - 'scripts/windows-package-source-closure.mjs'
+ - 'scripts/windows-package-source-closure.test.mjs'
+ # The packaged worker and its Windows boundary driver are built from
+ # these sources, so changes here must reach the packaged lifecycle gate.
+ - 'apps/desktop/scripts/copy-runtime-filesystem-worker.mjs'
+ - 'packages/runtime/scripts/build-filesystem-worker.mjs'
+ - 'packages/runtime/src/filesystem-worker/**'
+ - 'packages/runtime/src/sandbox/**'
+ - 'packages/runtime/src/path-containment.ts'
+ - 'packages/runtime/src/sandbox-boundary-path.ts'
+ - 'packages/runtime/src/apply-patch-file.ts'
+ - 'packages/runtime/src/child-fd-input.ts'
+ - 'packages/runtime/src/child-process-lifecycle.ts'
+ - 'packages/runtime/src/edit-replace.ts'
+ - 'packages/runtime/src/file-stable-write.ts'
+ - 'packages/runtime/src/image-file.ts'
+ - 'packages/runtime/src/process-tree-terminator.ts'
+ - 'packages/runtime/src/unified-diff.ts'
+ - 'packages/core/src/absolute-path.ts'
+ - 'packages/core/src/artifacts.ts'
+ - 'packages/core/src/attachments.ts'
+ - 'packages/core/src/permission-profile.ts'
+ - 'packages/core/src/permission-profile-compiler.ts'
+ - 'packages/core/src/sandbox-boundary.ts'
+ - 'packages/core/src/serialized-byte-length.ts'
+ - 'packages/core/src/windows-path.ts'
- 'experiments/windows-sandbox/launcher/**'
- 'experiments/windows-sandbox/*.ps1'
- '.gitattributes'
- 'package.json'
- 'package-lock.json'
- 'apps/desktop/resources/licenses/cargo/THIRD_PARTY_NOTICES.txt'
+ - '.github/workflows/release.yml'
- '.github/workflows/release-windows-check.yml'
+ # The list above is a pre-filter, not this lane's import closure, so a
+ # transitive edit it cannot match would otherwise first be observed on release
+ # day. Same pairing as the other two Windows lanes.
+ schedule:
+ - cron: '17 5 * * *'
workflow_dispatch:
permissions:
contents: read
+# Keyed on the pull request number rather than the ref, because `github.ref` is
+# refs/heads/main for the nightly and for a dispatch alike: one shared group
+# would let a dispatch queue behind the nightly and then be discarded while
+# still pending. Same fix the other two Windows lanes already carry.
concurrency:
- group: release-windows-check-${{ github.ref }}
+ group: release-windows-check-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
jobs:
package:
runs-on: windows-2025
- timeout-minutes: 90
+ timeout-minutes: 45
defaults:
run:
shell: bash
@@ -93,6 +135,9 @@ jobs:
- name: Install dependencies
run: npm ci
+ - name: Update stable Rust for Desktop native artifacts
+ run: rustup update stable --no-self-update
+
- name: Package the Windows installer and ZIP
run: npm run package:windows-x64
@@ -101,6 +146,19 @@ jobs:
version="$(node -p "require('./apps/desktop/package.json').version")"
npm run verify:windows-x64 -- "apps/desktop/release/Maka-${version}-win-x64.exe"
+ # The baseline is pinned by version, tag, asset name and SHA-256 in
+ # `scripts/windows-upgrade-baseline.json`, so it is immutable and the
+ # cache key is that file. Downloading it is this lane's most frequent
+ # failure — 14 of 36 across 300 runs, always on branches that could not
+ # have caused it, each false red costing another 25-minute Windows
+ # runner. A hit skips the network; the step below re-verifies the
+ # checksum either way, so a corrupt entry still fails loudly.
+ - name: Restore the pinned Windows upgrade baseline
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: artifacts/windows-upgrade-baseline
+ key: windows-upgrade-baseline-${{ hashFiles('scripts/windows-upgrade-baseline.json') }}
+
- name: Download and verify the pinned Windows upgrade baseline
id: previous
env:
@@ -111,6 +169,9 @@ jobs:
"$version" artifacts/windows-upgrade-baseline)"
echo "exe=$previous_exe" >> "$GITHUB_OUTPUT"
+ # The pinned release is only one input here. The other is the installer
+ # this run just built, which this step installs over it, so a pull request
+ # does decide the outcome.
- name: Exercise pinned-version upgrade and uninstall
run: |
version="$(node -p "require('./apps/desktop/package.json').version")"
@@ -128,6 +189,9 @@ jobs:
"apps/desktop/release/Maka-${version}-win-x64.exe" \
apps/desktop/release-autoupdate-next
+ # Consumes only what this run built — the candidate installer and the
+ # version-bumped one — so it is the pull request's own evidence that the
+ # `installer.nsh` Abort path still restores the previous installation.
- name: Prove deterministic mid-install failure rollback
run: |
version="$(node -p "require('./apps/desktop/package.json').version")"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 32d10e1806..565f571f88 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -29,16 +29,15 @@ permissions:
contents: read
concurrency:
- group: release
+ group: product-release
cancel-in-progress: false
jobs:
release-identity:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
version: ${{ steps.identity.outputs.version }}
- is_prerelease: ${{ steps.identity.outputs.is_prerelease }}
tag: ${{ steps.identity.outputs.tag }}
source_commit: ${{ steps.identity.outputs.source_commit }}
source_reference_tag: ${{ steps.identity.outputs.source_reference_tag }}
@@ -137,6 +136,9 @@ jobs:
- name: Audit shipped desktop closure
run: node scripts/audit-shipped-dependencies.mjs
+ - name: Update stable Rust for Desktop native artifacts
+ run: rustup update stable --no-self-update
+
- name: Write App Store Connect API key
if: matrix.platform == 'macos'
env:
@@ -184,6 +186,20 @@ jobs:
if: matrix.platform == 'macos'
run: npm run verify:macos-arm64 -- "apps/desktop/release/${{ needs.release-identity.outputs.dmg }}"
+ - name: Build the version-bumped macOS update
+ if: matrix.platform == 'macos'
+ env:
+ CSC_LINK: ${{ secrets.CSC_LINK }}
+ CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
+ run: npm run package:macos-autoupdate-next
+
+ - name: Verify macOS automatic update end to end
+ if: matrix.platform == 'macos'
+ run: |
+ npm run verify:macos-autoupdate -- \
+ "apps/desktop/release/Maka-${{ needs.release-identity.outputs.version }}-mac-arm64.zip" \
+ apps/desktop/release-autoupdate-next
+
# Windows has no Authenticode certificate yet, so this build is unsigned
# and there is nothing to notarize between packaging and verification.
- name: Package the Windows installer and ZIP
@@ -239,10 +255,10 @@ jobs:
- name: Upload the verified release assets
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
- name: release-${{ matrix.platform }}
+ name: release-desktop-${{ matrix.platform }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/release-assets
if-no-files-found: error
- retention-days: 7
+ retention-days: 30
- name: Remove temporary release credentials
if: always() && matrix.platform == 'macos'
@@ -284,7 +300,7 @@ jobs:
- name: Build the CLI production workspace closure
run: |
- npm --workspace maka-agent run pretest
+ npm --workspace maka-agent run build:workspace-deps
npm --workspace maka-agent run build
npm run check:cli-third-party-notices
@@ -335,10 +351,10 @@ jobs:
- name: Upload the verified CLI release assets
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
- name: release-cli-macos-arm64
+ name: release-cli-macos-arm64-${{ github.run_attempt }}
path: ${{ runner.temp }}/release-assets
if-no-files-found: error
- retention-days: 7
+ retention-days: 30
- name: Remove temporary release credentials
if: always()
@@ -348,7 +364,7 @@ jobs:
# One draft release carries both platforms, so it is created once, after
# every platform has been packaged and verified.
needs: [release-identity, desktop, cli-macos-arm64]
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: write
@@ -367,16 +383,47 @@ jobs:
- name: Download the verified release assets
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
- pattern: release-*
+ pattern: release-*-${{ github.run_attempt }}
path: release-assets
merge-multiple: true
+ - name: Set up the pinned release Node.js
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: ${{ needs.release-identity.outputs.node_version }}
+ package-manager-cache: false
+
+ - name: Select the pinned npm release toolchain
+ run: |
+ npm install --global --no-audit --no-fund "npm@${{ needs.release-identity.outputs.npm_version }}"
+ test "$(npm --version)" = "${{ needs.release-identity.outputs.npm_version }}"
+
+ - name: Install the release verifier dependencies
+ run: npm ci --ignore-scripts --no-audit --no-fund
+
- name: Verify the exact product artifact manifest
+ run: node scripts/product-release-artifacts.mjs verify release-assets
+
+ - name: Record the immutable publication evidence
+ env:
+ GITHUB_SHA: ${{ needs.release-identity.outputs.source_commit }}
+ SOURCE_REFERENCE_TAG: ${{ needs.release-identity.outputs.source_reference_tag }}
run: |
- node scripts/product-release-artifacts.mjs verify release-assets
- while IFS= read -r -d '' checksum; do
- (cd "$(dirname "$checksum")" && sha256sum -c "$(basename "$checksum")")
- done < <(find release-assets -type f -name '*.sha256' -print0)
+ node scripts/product-release-artifacts.mjs record \
+ release-assets \
+ "$RUNNER_TEMP/product-release.json" \
+ "$GITHUB_REPOSITORY" \
+ "$GITHUB_RUN_ID" \
+ "$GITHUB_RUN_ATTEMPT"
+
+ - name: Upload the immutable publication evidence
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: product-release-record-${{ github.run_attempt }}
+ path: ${{ runner.temp }}/product-release.json
+ if-no-files-found: error
+ compression-level: 0
+ retention-days: 30
- name: Revalidate the live ASF source reference
env:
@@ -403,7 +450,6 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.release-identity.outputs.version }}
- IS_PRERELEASE: ${{ needs.release-identity.outputs.is_prerelease }}
TAG: ${{ needs.release-identity.outputs.tag }}
SOURCE_COMMIT: ${{ needs.release-identity.outputs.source_commit }}
SOURCE_REFERENCE_TAG: ${{ needs.release-identity.outputs.source_reference_tag }}
@@ -414,15 +460,9 @@ jobs:
The Windows build is unsigned: SmartScreen warns on first launch, and the download has to be checked against its .sha256 file.
- Known limitations: Computer Use and managed-workspace execution are not included in this release. The ASF Desktop artifacts do not distribute a Git runtime."
+ These files are convenience binaries built from the approved ASF source release; they are not ASF release artifacts.
- classification=(--prerelease=false --latest=false)
- if [[ "$IS_PRERELEASE" == "true" ]]; then
- classification=(--prerelease --latest=false)
- elif [[ "$IS_PRERELEASE" != "false" ]]; then
- echo "Product prerelease classification must be true or false; found $IS_PRERELEASE" >&2
- exit 1
- fi
+ Known limitations: Computer Use and Gitoxide-backed workspace execution are not included in this release. The Desktop convenience artifacts do not distribute a Git runtime."
if gh release view "$TAG" >/dev/null 2>&1; then
is_draft="$(gh release view "$TAG" --json isDraft --jq .isDraft)"
@@ -434,7 +474,8 @@ jobs:
gh release create "$TAG" \
--draft \
--verify-tag \
- "${classification[@]}" \
+ --prerelease=false \
+ --latest=false \
--title "Maka ${VERSION}" \
--notes "$notes"
fi
@@ -444,12 +485,13 @@ jobs:
exit 1
fi
gh release edit "$TAG" \
- "${classification[@]}" \
+ --prerelease=false \
+ --latest=false \
--title "Maka ${VERSION}" \
--notes "$notes"
actual_prerelease="$(gh release view "$TAG" --json isPrerelease --jq .isPrerelease)"
- if [[ "$actual_prerelease" != "$IS_PRERELEASE" ]]; then
- echo "Release ${TAG} prerelease state is ${actual_prerelease}, expected ${IS_PRERELEASE}." >&2
+ if [[ "$actual_prerelease" != "false" ]]; then
+ echo "Release ${TAG} must not be a prerelease." >&2
exit 1
fi
asset_names="$(gh release view "$TAG" --json assets --jq '.assets[].name')"
@@ -482,7 +524,7 @@ jobs:
fi
done
release_state="$(gh release view "$TAG" --json isDraft,isPrerelease --jq '[.isDraft, .isPrerelease] | map(tostring) | join(" ")')"
- if [[ "$release_state" != "true $IS_PRERELEASE" ]]; then
+ if [[ "$release_state" != "true false" ]]; then
echo "Release ${TAG} changed state while assets were checked: ${release_state}." >&2
exit 1
fi
@@ -490,4 +532,7 @@ jobs:
gh release upload "$TAG" "${missing_assets[@]}"
fi
- echo "Draft release ${TAG} created from ${SOURCE_COMMIT}." >> "$GITHUB_STEP_SUMMARY"
+ {
+ echo "Draft release ${TAG} created from ${SOURCE_COMMIT}."
+ echo "Release workflow run: ${GITHUB_RUN_ID}, attempt: ${GITHUB_RUN_ATTEMPT}."
+ } >> "$GITHUB_STEP_SUMMARY"
diff --git a/.github/workflows/runtime-host-owner-platform.yml b/.github/workflows/runtime-host-owner-platform.yml
new file mode 100644
index 0000000000..2f1ecaeb4b
--- /dev/null
+++ b/.github/workflows/runtime-host-owner-platform.yml
@@ -0,0 +1,71 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: Runtime Host owner platforms
+
+on:
+ pull_request:
+ branches: [main]
+ paths:
+ - '.github/workflows/runtime-host-owner-platform.yml'
+ - 'package.json'
+ - 'package-lock.json'
+ - 'packages/storage/src/process-lifetime-file-update-lock.ts'
+ - 'packages/runtime-host/src/operator/local-deployment-owner.ts'
+ - 'packages/runtime-host/src/operator/update-package-evidence.ts'
+ - 'packages/runtime-host/src/__tests__/local-deployment-owner.test.ts'
+ - 'packages/runtime-host/src/__tests__/fixtures/local-deployment-owner-claim.ts'
+ - 'packages/runtime-host/tsconfig.owner-platform.json'
+ workflow_dispatch:
+
+concurrency:
+ group: runtime-host-owner-platform-${{ github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+permissions:
+ contents: read
+
+jobs:
+ owner:
+ name: owner (${{ matrix.os }})
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [windows-latest, macos-latest]
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 45
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: '24'
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build Runtime Host test dependencies
+ run: |
+ npm --workspace @maka/core run build
+ npm --workspace @maka/storage run build
+ npm exec -- tsc --project packages/runtime-host/tsconfig.owner-platform.json
+
+ - name: Test local deployment owner
+ run: node --test packages/runtime-host/.owner-platform-dist/__tests__/local-deployment-owner.test.js
diff --git a/.github/workflows/runtime-host-peer-admission.yml b/.github/workflows/runtime-host-peer-admission.yml
new file mode 100644
index 0000000000..0011480711
--- /dev/null
+++ b/.github/workflows/runtime-host-peer-admission.yml
@@ -0,0 +1,117 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: Runtime Host peer admission
+
+on:
+ pull_request:
+ paths:
+ - '.github/workflows/runtime-host-peer-admission.yml'
+ - 'deny.toml'
+ - 'native/runtime-host-peer/**'
+ - 'packages/cli/RUNTIME_HOST_PEER_DEPENDENCIES.rust.tsv'
+ - 'packages/cli/RUNTIME_HOST_PEER_THIRD_PARTY_NOTICES.txt'
+ - 'scripts/generate-runtime-host-peer-dependencies.mjs'
+ - 'scripts/generate-runtime-host-peer-notices.mjs'
+ - 'packages/runtime-host/src/transport/peer-native.ts'
+ - 'packages/runtime-host/src/server/peer-listener.ts'
+ push:
+ branches:
+ - main
+ paths:
+ - '.github/workflows/runtime-host-peer-admission.yml'
+ - 'deny.toml'
+ - 'native/runtime-host-peer/**'
+ - 'packages/cli/RUNTIME_HOST_PEER_DEPENDENCIES.rust.tsv'
+ - 'packages/cli/RUNTIME_HOST_PEER_THIRD_PARTY_NOTICES.txt'
+ - 'scripts/generate-runtime-host-peer-dependencies.mjs'
+ - 'scripts/generate-runtime-host-peer-notices.mjs'
+ - 'packages/runtime-host/src/transport/peer-native.ts'
+ - 'packages/runtime-host/src/server/peer-listener.ts'
+
+permissions:
+ contents: read
+
+concurrency:
+ group: runtime-host-peer-admission-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ test:
+ name: quality
+ runs-on: ubuntu-24.04
+ timeout-minutes: 45
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: 24.18.1
+ - name: Update stable Rust
+ run: rustup update stable --no-self-update
+ - id: rustc
+ name: Resolve Rust cache version
+ shell: bash
+ run: |
+ echo "version=$(rustc --version | cut -d ' ' -f 2)" >> "$GITHUB_OUTPUT"
+ echo "revision=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
+ {
+ echo "KACHE_CACHE_DIR=${{ runner.temp }}/kache"
+ echo "KACHE_RUNTIME_DIR=${{ runner.temp }}/kache-runtime"
+ echo "RUSTC_WRAPPER=kache"
+ } >> "$GITHUB_ENV"
+ - name: Install Kache
+ uses: taiki-e/install-action@1ed6d7be6168f6c9046541087ff549b6bc581fdf # v2
+ with:
+ tool: kache@0.16.0
+ - id: kache-cache
+ name: Restore Rust build cache
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: ${{ runner.temp }}/kache
+ key: kache-runtime-host-peer-quality-v0.16.0-${{ runner.os }}-${{ runner.arch }}-rust-${{ steps.rustc.outputs.version }}-${{ steps.rustc.outputs.revision }}
+ restore-keys: |
+ kache-runtime-host-peer-quality-v0.16.0-${{ runner.os }}-${{ runner.arch }}-rust-${{ steps.rustc.outputs.version }}-
+ - name: Check Rust formatting
+ working-directory: native/runtime-host-peer
+ run: cargo fmt --check
+ - name: Lint the native peer
+ working-directory: native/runtime-host-peer
+ run: cargo clippy --locked --all-targets -- -D warnings
+ - name: Install cargo-deny
+ uses: taiki-e/install-action@1ed6d7be6168f6c9046541087ff549b6bc581fdf # v2
+ with:
+ tool: cargo-deny@0.20.2
+ - name: Check Cargo dependency inventory
+ run: npm run check:runtime-host-peer-dependencies
+ - name: Check Cargo dependency licenses
+ run: cargo deny --manifest-path native/runtime-host-peer/Cargo.toml --locked --exclude-dev check licenses
+ - name: Check Cargo dependency notices
+ run: npm run check:runtime-host-peer-notices
+ - name: Test the native peer
+ working-directory: native/runtime-host-peer
+ run: cargo test --locked
+ - name: Report Rust build cache
+ shell: bash
+ run: kache report --format github >> "$GITHUB_STEP_SUMMARY"
+ - name: Save Rust build cache
+ if: github.ref_name == github.event.repository.default_branch
+ uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: ${{ runner.temp }}/kache
+ key: ${{ steps.kache-cache.outputs.cache-primary-key }}
diff --git a/.github/workflows/take.yml b/.github/workflows/take.yml
new file mode 100644
index 0000000000..0f2cb5cfe2
--- /dev/null
+++ b/.github/workflows/take.yml
@@ -0,0 +1,179 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: Assign/unassign the issue via `take` or `untake` comment
+on:
+ issue_comment:
+ types: created
+
+permissions:
+ issues: write
+
+jobs:
+ issue_assign:
+ runs-on: ubuntu-slim
+ if: (!github.event.issue.pull_request) && (github.event.comment.body == 'take' || github.event.comment.body == 'untake')
+ concurrency:
+ group: ${{ github.workflow }}-${{ github.event.issue.number }}
+ queue: max
+ steps:
+ - name: Take or untake issue
+ env:
+ COMMENT_BODY: ${{ github.event.comment.body }}
+ ISSUE_NUMBER: ${{ github.event.issue.number }}
+ USER_LOGIN: ${{ github.event.comment.user.login }}
+ REPO: ${{ github.repository }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ API_VERSION="2026-03-10"
+ ISSUE_ENDPOINT="repos/$REPO/issues/$ISSUE_NUMBER"
+ ASSIGNEES_ENDPOINT="$ISSUE_ENDPOINT/assignees"
+ COMMENTS_ENDPOINT="$ISSUE_ENDPOINT/comments"
+
+ post_comment() {
+ local body="$1"
+ gh api --silent \
+ --method POST \
+ --header "X-GitHub-Api-Version: $API_VERSION" \
+ "$COMMENTS_ENDPOINT" \
+ --raw-field "body=$body"
+ }
+
+ reply() {
+ local body="$1"
+ echo "$body"
+ post_comment "$body"
+ }
+
+ fail_with_comment() {
+ local body="$1"
+ echo "$body" >&2
+ if ! post_comment "$body"; then
+ echo "Could not post the failure message to issue $ISSUE_NUMBER" >&2
+ fi
+ exit 1
+ }
+
+ if [[ "$COMMENT_BODY" == "take" ]]; then
+ if ! CURRENT_ASSIGNEES="$(
+ gh api \
+ --header "X-GitHub-Api-Version: $API_VERSION" \
+ "$ISSUE_ENDPOINT" \
+ --jq '[.assignees[].login]'
+ )"; then
+ fail_with_comment \
+ "I could not inspect the current assignees. Please try again later."
+ fi
+
+ if jq -e --arg user "$USER_LOGIN" \
+ 'any(.[]; . != $user)' <<<"$CURRENT_ASSIGNEES" >/dev/null; then
+ CURRENT_HOLDERS="$(
+ jq -r --arg user "$USER_LOGIN" \
+ '[.[] | select(. != $user)] | join(", ")' \
+ <<<"$CURRENT_ASSIGNEES"
+ )"
+ reply \
+ "Issue #$ISSUE_NUMBER is already assigned to $CURRENT_HOLDERS, so it was not reassigned."
+ exit 0
+ fi
+
+ if jq -e --arg user "$USER_LOGIN" \
+ 'index($user) != null' <<<"$CURRENT_ASSIGNEES" >/dev/null; then
+ echo "Issue #$ISSUE_NUMBER is already assigned to $USER_LOGIN."
+ exit 0
+ fi
+
+ echo "Assigning issue $ISSUE_NUMBER to $USER_LOGIN"
+ if ! ASSIGNMENT_RESPONSE="$(
+ gh api \
+ --method POST \
+ --header "X-GitHub-Api-Version: $API_VERSION" \
+ "$ASSIGNEES_ENDPOINT" \
+ --field "assignees[]=$USER_LOGIN"
+ )"; then
+ fail_with_comment \
+ "I could not assign this issue because the GitHub API request failed. Please try again later."
+ fi
+
+ if ! jq -e '.assignees | type == "array"' \
+ <<<"$ASSIGNMENT_RESPONSE" >/dev/null; then
+ fail_with_comment \
+ "I could not verify the new assignee. Please try again later."
+ fi
+
+ if ! jq -e --arg user "$USER_LOGIN" \
+ '.assignees | map(.login) | index($user) != null' \
+ <<<"$ASSIGNMENT_RESPONSE" >/dev/null; then
+ reply \
+ "GitHub did not add $USER_LOGIN as an assignee. The account may not be assignable in this repository."
+ exit 0
+ fi
+
+ if ! jq -e --arg user "$USER_LOGIN" \
+ '.assignees | length == 1 and .[0].login == $user' \
+ <<<"$ASSIGNMENT_RESPONSE" >/dev/null; then
+ CURRENT_HOLDERS="$(
+ jq -r --arg user "$USER_LOGIN" \
+ '[.assignees[].login | select(. != $user)] | join(", ")' \
+ <<<"$ASSIGNMENT_RESPONSE"
+ )"
+
+ if ! ROLLBACK_RESPONSE="$(
+ gh api \
+ --method DELETE \
+ --header "X-GitHub-Api-Version: $API_VERSION" \
+ "$ASSIGNEES_ENDPOINT" \
+ --field "assignees[]=$USER_LOGIN"
+ )"; then
+ fail_with_comment \
+ "Another assignee appeared while take was running, and I could not undo the conflicting assignment."
+ fi
+
+ if ! jq -e --arg user "$USER_LOGIN" \
+ '.assignees | type == "array" and (map(.login) | index($user) == null)' \
+ <<<"$ROLLBACK_RESPONSE" >/dev/null; then
+ fail_with_comment \
+ "Another assignee appeared while take was running, and I could not verify that the conflicting assignment was undone."
+ fi
+
+ reply \
+ "Issue #$ISSUE_NUMBER was assigned to $CURRENT_HOLDERS while take was running, so it was not reassigned."
+ exit 0
+ fi
+ elif [[ "$COMMENT_BODY" == "untake" ]]; then
+ echo "Unassigning issue $ISSUE_NUMBER from $USER_LOGIN"
+ if ! UNASSIGNMENT_RESPONSE="$(
+ gh api \
+ --method DELETE \
+ --header "X-GitHub-Api-Version: $API_VERSION" \
+ "$ASSIGNEES_ENDPOINT" \
+ --field "assignees[]=$USER_LOGIN"
+ )"; then
+ fail_with_comment \
+ "I could not unassign this issue because the GitHub API request failed. Please try again later."
+ fi
+
+ if ! jq -e --arg user "$USER_LOGIN" \
+ '.assignees | type == "array" and (map(.login) | index($user) == null)' \
+ <<<"$UNASSIGNMENT_RESPONSE" >/dev/null; then
+ fail_with_comment \
+ "I could not verify that $USER_LOGIN was unassigned. Please try again later."
+ fi
+ fi
diff --git a/.github/workflows/windows-baseline.yml b/.github/workflows/windows-baseline.yml
index a2f812c7a3..8a6fd69cd1 100644
--- a/.github/workflows/windows-baseline.yml
+++ b/.github/workflows/windows-baseline.yml
@@ -149,7 +149,7 @@ jobs:
# Full packages/storage test:dist is ~10 minutes on windows-latest and
# mostly duplicates the Linux unit lane. Baseline keeps process/path/
- # lock-sensitive gates here. Release-blocking crash evidence belongs to
+ # lock-sensitive gates here. Crash and owner-death evidence belongs to
# windows-recovery.yml, so this diagnostic lane does not duplicate it.
# Bump concurrency carefully — several suites spawn child processes and
# fight for disk under high fan-out.
@@ -167,7 +167,6 @@ jobs:
'packages/storage/dist/__tests__/write-queue.test.js',
'packages/storage/dist/__tests__/marker-file.test.js',
'packages/storage/dist/__tests__/workspace-root.test.js',
- 'packages/storage/dist/__tests__/managed-workspace-owner.test.js',
'packages/storage/dist/__tests__/workspace-identity.test.js'
)
node.exe --test --test-concurrency=2 --test-force-exit --test-timeout=60000 `
diff --git a/.github/workflows/windows-recovery.yml b/.github/workflows/windows-recovery.yml
index 90dc9b1e5c..cbf1ea3791 100644
--- a/.github/workflows/windows-recovery.yml
+++ b/.github/workflows/windows-recovery.yml
@@ -17,11 +17,137 @@
name: Windows recovery
+# The paths below name only what a Windows runner can prove and the required
+# `test` lane cannot. Two groups.
+#
+# First, how the toolchain behaves here: `npm ci` resolution, what the
+# dependency patches and the Electron installer produce, what the clean step
+# removes, and the Local IPC trust boundary, a PowerShell script with no other
+# caller.
+#
+# Second, every file in the import closure of the suites below that branches on
+# `process.platform === 'win32'`. Those branches are skipped on Linux by
+# construction, so `test` cannot go red on them however carefully it runs.
+# `assertNoWindowsAlternateStreams` is the example that matters: it is the one
+# regression this lane has caught that no other lane could have. That second
+# group is generated rather than curated, because a hand-picked list cannot be
+# shown to be complete and this one has to be.
+#
+# What is deliberately absent is the portable rest of those workspaces. Every
+# source directory in the closure used to be here, which put this lane on 118 of
+# the last 200 merges against 40 now. Over 300 runs it never once produced a
+# pull-request red that `test` had not already produced on the same commit,
+# usually at the very same step — the portable half of these suites is ordinary
+# TypeScript, so a defect in it fails on Linux first and blocks the merge there.
+#
+# The wide filter also did not prevent the one regression it is tempting to
+# cite. #4400 changed `packages/storage/src/**`, matched that filter, ran this
+# lane three times on the pull request, and passed all three; the alternate
+# stream failure appeared only on the main push afterwards. That is a stale-base
+# interaction, which no path filter can see and which the unfiltered push below
+# exists to catch. That trigger stays unfiltered for the same reason
+# `required_status_checks` is `strict: false`, and because GitHub evaluates a
+# path filter against the first 300 files of a diff only — a repository-wide
+# sweep being exactly the change that touches every recovery authority at once.
on:
+ pull_request:
+ branches: [main]
+ paths:
+ - 'package-lock.json'
+ - 'patches/**'
+ - 'scripts/apply-dependency-patches.mjs'
+ - 'scripts/install-electron-with-retry.mjs'
+ - 'scripts/run-electron-installer.cjs'
+ - 'scripts/clean-build.mjs'
+ - 'scripts/clean-paths.mjs'
+ - 'scripts/windows-runtime-host-local-ipc-trust.ps1'
+ # Every file in the import closure of the suites below that branches on
+ # `process.platform === 'win32'`. Generated, not curated:
+ # `scripts/windows-package-source-closure.test.mjs` recomputes this set
+ # from the dist tests the steps run and fails on any difference in either
+ # direction.
+ - 'packages/core/src/diagnostic-log.ts'
+ - 'packages/runtime-host/src/__tests__/control-endpoint.test.ts'
+ - 'packages/runtime-host/src/__tests__/fixtures/endpoint-hygiene.ts'
+ - 'packages/runtime-host/src/__tests__/skill-catalog-protocol.test.ts'
+ - 'packages/runtime-host/src/__tests__/skill-catalog-repository.test.ts'
+ - 'packages/runtime-host/src/__tests__/skill-catalog-transaction.test.ts'
+ - 'packages/runtime-host/src/__tests__/skill-catalog-two-client-uds.test.ts'
+ - 'packages/runtime-host/src/client/client-instance-identity.ts'
+ - 'packages/runtime-host/src/client/host-profile.ts'
+ - 'packages/runtime-host/src/client/ssh-tunnel.ts'
+ - 'packages/runtime-host/src/client/wsl-control.ts'
+ - 'packages/runtime-host/src/control/access-credential-delivery.ts'
+ - 'packages/runtime-host/src/control/endpoint.ts'
+ - 'packages/runtime-host/src/control/registration.ts'
+ - 'packages/runtime-host/src/control/startup-diagnostic.ts'
+ - 'packages/runtime-host/src/operator/local-deployment-owner.ts'
+ - 'packages/runtime-host/src/operator/managed-deployment.ts'
+ - 'packages/runtime-host/src/peer-mesh/owner.ts'
+ - 'packages/runtime-host/src/peer-mesh/store.ts'
+ - 'packages/runtime-host/src/protocol/host-status.ts'
+ - 'packages/runtime-host/src/protocol/skill-catalog.ts'
+ - 'packages/runtime-host/src/server/access-credential-store.ts'
+ - 'packages/runtime-host/src/server/skill-catalog-repository.ts'
+ - 'packages/runtime-host/src/server/skill-catalog-transaction.ts'
+ - 'packages/runtime/src/__tests__/runtime-continuation-crash.test.ts'
+ - 'packages/runtime/src/__tests__/runtime-resume-crash.test.ts'
+ - 'packages/runtime/src/builtin-tools.ts'
+ - 'packages/runtime/src/file-stable-write.ts'
+ - 'packages/runtime/src/filesystem-worker/client.ts'
+ - 'packages/runtime/src/filesystem-worker/launch-spec.ts'
+ - 'packages/runtime/src/filesystem-worker/process-runner.ts'
+ - 'packages/runtime/src/pipe-process-driver.ts'
+ - 'packages/runtime/src/process-tree-terminator.ts'
+ - 'packages/runtime/src/pty-process-driver.ts'
+ - 'packages/runtime/src/sandbox-boundary-declaration.ts'
+ - 'packages/runtime/src/sandbox/default-sandbox-manager.ts'
+ - 'packages/runtime/src/sandbox/sandbox-manager.ts'
+ - 'packages/runtime/src/sandbox/windows-profile.ts'
+ - 'packages/runtime/src/sandbox/windows-sandbox.ts'
+ - 'packages/runtime/src/shell-detect.ts'
+ - 'packages/runtime/src/shell-exec.ts'
+ - 'packages/storage/src/__tests__/managed-dependency-environment.test.ts'
+ - 'packages/storage/src/__tests__/root-authority.test.ts'
+ - 'packages/storage/src/artifact-store.ts'
+ - 'packages/storage/src/artifact-writer-bootstrap-lock.ts'
+ - 'packages/storage/src/artifact-writer-lock.ts'
+ - 'packages/storage/src/credential-store.ts'
+ - 'packages/storage/src/file-lifetime-owner.ts'
+ - 'packages/storage/src/managed-dependency-environment.ts'
+ - 'packages/storage/src/marker-file.ts'
+ - 'packages/storage/src/memory-bundle-io.ts'
+ - 'packages/storage/src/native-file-lock.ts'
+ - 'packages/storage/src/root-authority.ts'
+ - 'packages/storage/src/runtime-policy/document-io.ts'
+ - 'packages/storage/src/sqlite-long-term-memory-store.ts'
+ - 'packages/storage/src/stable-storage.ts'
+ - '.github/workflows/windows-recovery.yml'
+ # Unfiltered on purpose: required_status_checks is `strict: false`, so a pull
+ # request goes green against a stale base and only the merged result proves
+ # two independently green halves still agree. It is also the backstop for the
+ # 300-file case above. The nightly alone would find either a day later,
+ # against a batch of commits instead of one.
+ push:
+ branches: [main]
+ schedule:
+ # Offset from windows-sandbox-w0 so the Windows lanes do not overlap.
+ - cron: '17 8 * * *'
workflow_dispatch:
+# Pull request pushes supersede each other, keyed on the pull request number
+# because github.head_ref is a bare branch name two forks can share: a second
+# contributor pushing to their own `main` would otherwise cancel the first
+# contributor's run, and a cancelled check is not a failed one.
+# Scheduled, manual and main-push runs fall back to the run id, since
+# github.ref is refs/heads/main for all three and one shared group would let a
+# dispatch queue behind the nightly and then be discarded while still pending.
+# That deliberately stops main pushes from superseding each other: this lane is
+# the only place a merged Windows regression is observed, so every merge needs
+# its own evidence rather than only the newest one surviving.
concurrency:
- group: windows-recovery-${{ github.ref }}
+ group: windows-recovery-${{ github.event.pull_request.number || github.run_id }}
+ cancel-in-progress: true
permissions:
contents: read
@@ -30,7 +156,7 @@ jobs:
windows_recovery:
name: windows_recovery
runs-on: windows-latest
- timeout-minutes: 30
+ timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
@@ -47,12 +173,60 @@ jobs:
- name: Build test artifacts
run: npm.cmd run build:test
+ - name: Verify managed dependency alternate streams
+ shell: pwsh
+ run: |
+ node.exe --test --test-reporter=tap --test-concurrency=1 `
+ --test-name-pattern="NTFS alternate stream" `
+ packages/storage/dist/__tests__/managed-dependency-environment.test.js `
+ 2>&1 | Tee-Object -FilePath "$env:RUNNER_TEMP/managed-dependency-ads.tap"
+ $exitCode = $LASTEXITCODE
+ if ($exitCode -ne 0) { exit $exitCode }
+ $output = Get-Content "$env:RUNNER_TEMP/managed-dependency-ads.tap"
+ if ($output -notcontains '# tests 3' -or $output -notcontains '# pass 3' -or $output -notcontains '# skipped 0') {
+ Write-Error 'Managed dependency ADS gate did not run exactly three passing Windows tests'
+ exit 1
+ }
+
+ - name: Verify root initialization replacement race
+ shell: pwsh
+ run: |
+ node.exe --test --test-reporter=tap --test-concurrency=1 `
+ --test-name-pattern="rejects replacement before opening the temporary marker" `
+ packages/storage/dist/__tests__/root-authority.test.js `
+ 2>&1 | Tee-Object -FilePath "$env:RUNNER_TEMP/root-initialization-race.tap"
+ $exitCode = $LASTEXITCODE
+ if ($exitCode -ne 0) { exit $exitCode }
+ $output = Get-Content "$env:RUNNER_TEMP/root-initialization-race.tap"
+ if ($output -notcontains '# tests 1' -or $output -notcontains '# pass 1' -or $output -notcontains '# skipped 0') {
+ Write-Error 'Root initialization race gate did not run exactly one passing Windows test'
+ exit 1
+ }
+
- name: Verify Runtime Host Local IPC trust boundary
shell: pwsh
run: |
node.exe --test packages/runtime-host/dist/__tests__/control-endpoint.test.js
./scripts/windows-runtime-host-local-ipc-trust.ps1
+ - name: Verify Skill catalog on Windows
+ shell: pwsh
+ run: |
+ node.exe --test --test-reporter=tap --test-concurrency=1 `
+ packages/runtime-host/dist/__tests__/skill-catalog-coordinator.test.js `
+ packages/runtime-host/dist/__tests__/skill-catalog-protocol.test.js `
+ packages/runtime-host/dist/__tests__/skill-catalog-repository.test.js `
+ packages/runtime-host/dist/__tests__/skill-catalog-transaction.test.js `
+ packages/runtime-host/dist/__tests__/skill-catalog-two-client-uds.test.js `
+ 2>&1 | Tee-Object -FilePath "$env:RUNNER_TEMP/skill-catalog.tap"
+ $exitCode = $LASTEXITCODE
+ if ($exitCode -ne 0) { exit $exitCode }
+ $output = Get-Content "$env:RUNNER_TEMP/skill-catalog.tap"
+ if ($output -notcontains '# tests 91' -or $output -notcontains '# pass 91' -or $output -notcontains '# skipped 0') {
+ Write-Error 'Skill catalog gate did not run exactly 91 passing Windows tests'
+ exit 1
+ }
+
- name: Verify SQLite crash recovery
shell: pwsh
run: |
@@ -82,20 +256,3 @@ jobs:
Write-Error 'Runtime Host recovery gate did not run exactly two passing tests'
exit 1
}
-
- - name: Verify managed workspace crash recovery
- shell: pwsh
- run: |
- $env:MAKA_STORAGE_STRESS = '1'
- node.exe --test --test-reporter=tap --test-concurrency=1 `
- --test-name-pattern="real process crash|real crash|real-process crash|crash after baseline ref publication" `
- packages/storage/dist/__tests__/managed-workspace-baseline.test.js `
- packages/storage/dist/__tests__/git-workspace-service.test.js `
- 2>&1 | Tee-Object -FilePath "$env:RUNNER_TEMP/managed-workspace-recovery.tap"
- $exitCode = $LASTEXITCODE
- if ($exitCode -ne 0) { exit $exitCode }
- $output = Get-Content "$env:RUNNER_TEMP/managed-workspace-recovery.tap"
- if ($output -notcontains '# tests 12' -or $output -notcontains '# pass 12' -or $output -notcontains '# skipped 0') {
- Write-Error 'Managed workspace recovery gate did not run exactly twelve passing tests'
- exit 1
- }
diff --git a/.github/workflows/windows-sandbox-w0.yml b/.github/workflows/windows-sandbox-w0.yml
index 6ac97ef2b2..d20131f0cc 100644
--- a/.github/workflows/windows-sandbox-w0.yml
+++ b/.github/workflows/windows-sandbox-w0.yml
@@ -48,7 +48,7 @@ jobs:
protocol:
name: windows_sandbox_w0_protocol
runs-on: windows-2025
- timeout-minutes: 25
+ timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
@@ -57,6 +57,29 @@ jobs:
with:
node-version: '24'
cache: npm
+ - id: rustc
+ name: Resolve Rust cache version
+ shell: bash
+ run: |
+ echo "version=$(rustc --version | cut -d ' ' -f 2)" >> "$GITHUB_OUTPUT"
+ echo "revision=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
+ {
+ echo "KACHE_CACHE_DIR=${{ runner.temp }}/kache"
+ echo "KACHE_RUNTIME_DIR=${{ runner.temp }}/kache-runtime"
+ echo "RUSTC_WRAPPER=kache"
+ } >> "$GITHUB_ENV"
+ - name: Install Kache
+ uses: taiki-e/install-action@1ed6d7be6168f6c9046541087ff549b6bc581fdf # v2
+ with:
+ tool: kache@0.16.0
+ - id: kache-cache
+ name: Restore Rust build cache
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: ${{ runner.temp }}/kache
+ key: kache-windows-sandbox-launcher-v0.16.0-${{ runner.os }}-${{ runner.arch }}-rust-${{ steps.rustc.outputs.version }}-${{ steps.rustc.outputs.revision }}
+ restore-keys: |
+ kache-windows-sandbox-launcher-v0.16.0-${{ runner.os }}-${{ runner.arch }}-rust-${{ steps.rustc.outputs.version }}-
- name: Record atomic launcher capability
shell: pwsh
run: ./experiments/windows-sandbox/atomic-launch-capability.ps1
@@ -69,6 +92,15 @@ jobs:
- name: Verify launcher protocol and broker authorization
working-directory: experiments/windows-sandbox/launcher
run: cargo test --locked
+ - name: Report Rust build cache
+ shell: bash
+ run: kache report --format github >> "$GITHUB_STEP_SUMMARY"
+ - name: Save Rust build cache
+ if: github.ref_name == github.event.repository.default_branch
+ uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: ${{ runner.temp }}/kache
+ key: ${{ steps.kache-cache.outputs.cache-primary-key }}
- name: Verify secure broker pipe
shell: pwsh
run: ./experiments/windows-sandbox/broker-pipe-smoke.ps1
diff --git a/.gitignore b/.gitignore
index 0f6f738912..46829faa83 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,8 @@ storybook-static
# `link-workspace-packages` setting.
pnpm-lock.yaml
*.tsbuildinfo
+/packages/core/src/model-metadata.generated.ts
+/packages/runtime/src/telemetry/model-pricing.generated.ts
__pycache__/
*.pyc
.agents-workspace-data
@@ -24,6 +26,10 @@ docs/assets/
apps/desktop/tests/real-window-smoke/
deepseek.key
+# Built only by the dedicated Gitoxide helper lane; normal workspace tests do not use Cargo.
+/native/gitoxide-helper/target/
+/native/runtime-host-peer/target/
+
# Generated Computer Use executor binary; provenance metadata stays tracked.
apps/desktop/resources/bin/
# Rebuilt from experiments/windows-sandbox by scripts/package-windows-x64.mjs.
@@ -33,6 +39,8 @@ apps/desktop/bundled-git.json
# Generated desktop release inputs and outputs.
apps/desktop/resources/tools/
apps/desktop/release/
+apps/desktop/release-autoupdate-next/
apps/desktop/release-sources/
packages/cli/release/
+packages/cli/.development/
release/asf/
diff --git a/.maka-shots/workhub-context-after.png b/.maka-shots/workhub-context-after.png
new file mode 100644
index 0000000000..8b82852c06
Binary files /dev/null and b/.maka-shots/workhub-context-after.png differ
diff --git a/.maka-shots/workhub-context-before.png b/.maka-shots/workhub-context-before.png
new file mode 100644
index 0000000000..a2903e2e52
Binary files /dev/null and b/.maka-shots/workhub-context-before.png differ
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 8973fd8746..07bafbf32d 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -27,7 +27,7 @@ Maka has one execution authority: Runtime Host. Desktop, TUI, CLI, bots, and eva
flowchart LR
C["Desktop / TUI / CLI / Bot"] --> H["Runtime Host"]
H --> S["SessionManager"]
- S --> R["AgentRun + Runtime Runner"]
+ S --> R["AgentRun + RuntimeKernel"]
R --> T["Tool Runtime"]
R --> L["Runtime Event Log"]
S --> G["Agent Graph Control Plane"]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 99af10ef4c..acffc8fe45 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,8 +28,26 @@
- Added `/transcript` to browse long TUI sessions without depending on terminal
scrollback, with line, page, and first/last navigation.
+### Fixed
+
+- Fixed a renderer crash dialog reporting React error #185 ("Maximum update depth
+ exceeded") coming from the composer's prompt-history inline completion (#4117): the
+ offer engine the 0.1.11 composer fed could flip-flop its announcement state on
+ real-layout measurements until React hit its nested-update limit, which surfaced as
+ the crash dialog. The unstable completion wiring was removed from the composer
+ (#3292), Astryx 0.5.0 no longer ships the engine (#3755), and regression tests now
+ keep that seam closed.
+
### Changed
+- Made typed `request()` the sole direct Runtime Host operation API; removed the 17 forwarding
+ aliases from direct and reconnecting connections while preserving status validation,
+ subscriptions, capabilities, listeners, lifecycle, and close behavior.
+- Collapsed the RuntimeRunner/Flow/Invocation shell into `RuntimeKernel`; backend dispatch,
+ terminal coalescing, stop/drain, and durable continuation admission now have one production
+ owner, immutable request snapshots remain enforced at AgentRun acceptance and backend dispatch,
+ and SessionEvent-to-RuntimeEvent conversion remains a pure mapper.
+- Retired the Task Ledger domain: SessionTodo is now the sole authority for in-session work items, and the operational-state schema drops the `workflow_task_ledger_events` table on first open. **Unfinished Tasks are not migrated and are permanently deleted.** This affects workspaces last opened by `v0.1.0` through `v0.1.11`, `cli-v0.1.0-beta.1`, `v0.2.0-incubating-rc1`, or a `v0.2.0-dev` build; those releases wrote Tasks to a table that no shipped build ever bridged into SessionTodo. Before opening such a workspace with this build, finish or export the Tasks you still need, or copy the workspace's `runtime.sqlite` aside — the migration removes the only live copy, so afterwards recovery requires a backup made in advance.
- Unified context management under one Runtime-owned policy. `MAKA_CONTEXT_*` environment overrides no longer tune or disable compaction and Tool Result pruning; model-visible archive placeholders are read on demand through bounded `ArchiveRead` calls instead of eager hydration. Previously supported overrides are ignored on upgrade: if Tool Result pruning was set to `off`, pruning is re-enabled, and there is currently no supported replacement opt-out.
## 0.1.11 - 2026-08-18
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1deb36ef9f..89ec9df3eb 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -25,6 +25,8 @@
Bug fixes, model provider support, tests, performance work, and documentation merge most readily. Pick something up from [`help wanted`](https://github.com/apache/maka/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) · [`good first issue`](https://github.com/apache/maka/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) · [`bug`](https://github.com/apache/maka/issues?q=is%3Aissue+is%3Aopen+label%3Abug) · [`enhancement`](https://github.com/apache/maka/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement) and claim it in a comment. Use the **Bug report** or **Feature request** template for new issues; report security problems through [SECURITY.md](./SECURITY.md), never as a public issue. Questions, ideas, and not-yet-actionable proposals belong in [Discussions](https://github.com/apache/maka/discussions), which reaches the whole team by email.
+To self-assign an unclaimed issue, post a comment whose entire body is exactly `take`; post `untake` to remove your assignment. Other claim text does not trigger the workflow.
+
Project direction, governance, and material product decisions are discussed publicly on [`dev@maka.apache.org`](https://lists.apache.org/list.html?dev@maka.apache.org) before implementation; implementation-level decisions may live in the pull request.
## Human ownership and AI attribution
@@ -43,14 +45,14 @@ Submit only work you have the right to contribute, and record third-party source
## Quick start
-Requires Node `>=22.19.0` and npm `11.19.0` (root `package.json`); desktop work needs macOS Apple Silicon.
+Requires Node `>=22.19.0` and npm `11.19.0` (root `package.json`). Direct Peer or Peer Mesh Desktop development additionally needs Rust stable 1.98 or newer and Xcode Command Line Tools on macOS, or MSVC Build Tools on Windows.
```sh
git clone https://github.com/apache/maka.git
cd maka
npm install # root only — never inside a workspace
npm run build # builds every workspace in dependency order
-npm --workspace @maka/core test
+npm --workspace @maka/core run test:dist
```
## Developing Maka
@@ -58,10 +60,10 @@ npm --workspace @maka/core test
```sh
npm run dev # desktop app with HMR
npm run cli:dev # TUI; `npm run cli:dev -- run "…"` runs one non-interactive turn
-npm test # all workspaces, or: npm --workspace @maka/core test
+npm test # all workspaces, or: npm --workspace @maka/core run test:dist
```
-Building a single workspace only succeeds when its dependencies are already built — when unsure, build from the root. Tests run against compiled output in `dist/`; each workspace's `test` script cleans, builds, then runs `node --test`. Always go through it.
+Building a single workspace only succeeds when its dependencies are already built — when unsure, build from the root. Tests run against compiled output in `dist/`, so `test:dist` covers whatever the last build produced; rebuild before running it. `npm test` from the root does both.
Before pushing, match CI locally:
diff --git a/CONTRIBUTING.zh-CN.md b/CONTRIBUTING.zh-CN.md
index b945843117..f7c4d3d041 100644
--- a/CONTRIBUTING.zh-CN.md
+++ b/CONTRIBUTING.zh-CN.md
@@ -25,6 +25,8 @@
缺陷修复、模型供应商支持、测试、性能优化和文档最容易被合并。想找活干,从 [`help wanted`](https://github.com/apache/maka/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) · [`good first issue`](https://github.com/apache/maka/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) · [`bug`](https://github.com/apache/maka/issues?q=is%3Aissue+is%3Aopen+label%3Abug) · [`enhancement`](https://github.com/apache/maka/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement) 里挑一个,留言认领。提 issue 走 **Bug report** 或 **Feature request** 模板;安全问题走 [SECURITY.md](./SECURITY.md) 的私密流程,不要开公开 issue。提问、想法和还不成熟的提案发到 [Discussions](https://github.com/apache/maka/discussions)——它会自动进到大家的邮箱,比 issue 更容易被看到。
+若要自助认领一个尚未分配的 issue,请单独评论 `take`(评论正文只能是这个单词);评论 `untake` 可以解除自己的认领。其他认领文字不会触发该工作流。
+
项目方向、治理和重大产品决策在实施前于开发邮件列表 [`dev@maka.apache.org`](https://lists.apache.org/list.html?dev@maka.apache.org) 上公开讨论;实现层面的技术决策可以在 PR 中讨论。
## 人类责任与 AI 归因
@@ -43,14 +45,14 @@
## 快速开始
-需要 Node `>=22.19.0` 和 npm `11.19.0`(见根 `package.json`);桌面端开发需要 macOS Apple Silicon。
+需要 Node `>=22.19.0` 和 npm `11.19.0`(见根 `package.json`)。开发 Desktop Direct Peer 或 Peer Mesh 还需要 Rust stable 1.98 或更高版本,以及 macOS 的 Xcode Command Line Tools 或 Windows 的 MSVC Build Tools。
```sh
git clone https://github.com/apache/maka.git
cd maka
npm install # 只在根目录装 —— 不要在某个 workspace 里跑
npm run build # 按依赖顺序构建全部 workspace
-npm --workspace @maka/core test
+npm --workspace @maka/core run test:dist
```
## 开发
@@ -58,10 +60,10 @@ npm --workspace @maka/core test
```sh
npm run dev # 带 HMR 的桌面应用
npm run cli:dev # TUI;`npm run cli:dev -- run "…"` 非交互地跑一个 Turn
-npm test # 全部 workspace,或:npm --workspace @maka/core test
+npm test # 全部 workspace,或:npm --workspace @maka/core run test:dist
```
-只有依赖都已构建好时,单独构建某个 workspace 才会成功——拿不准就从根目录构建。测试跑的是 `dist/` 里的编译产物;每个 workspace 的 `test` 脚本都会先清理、再构建,然后执行 `node --test`。务必走它。
+只有依赖都已构建好时,单独构建某个 workspace 才会成功——拿不准就从根目录构建。测试跑的是 `dist/` 里的编译产物,`test:dist` 覆盖的是最近一次构建的结果,跑之前先重新构建。根目录的 `npm test` 会把两步都做掉。
推送前先在本地对齐 CI:
diff --git a/DESIGN.md b/DESIGN.md
index 8918cf1d14..452e810153 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -113,6 +113,7 @@ Prose uses exactly three tiers — `--foreground`, `--foreground-secondary`, `--
- **The Three-Tier Reading Rule.** Prose uses primary, secondary, or muted. Neutral washes are surfaces, not extra text tiers. `--foreground-dimmed` is retired — its call sites name secondary directly — and must never come back with a definition of its own (contract-tested).
- **The One Colorspace Rule.** Every derivation inside a token family uses one colorspace (`oklch` for ink, contract-tested). Mixing `srgb` and `oklch` derivations produces "same literal, different value" drift, and the ladder is where it bites hardest because its rungs only mean anything relative to each other.
- **Astryx's `secondary` is not this ladder's secondary.** `Text color="secondary"` reads `--color-text-secondary`, which resolves to a fixed neutral literal outside the containers `astryx-mount.css` bridges and to `--muted-foreground` inside them. Neither is `--foreground-secondary`. One word, three results — so pick an ink tier by the token, and treat a primitive's color prop as its own vocabulary.
+- **`--color-text-disabled` is a deliberate exemption, not an oversight.** Astryx's `light-dark(#a3a3a3, #525252)` measures 2.52:1 / 2.29:1 — under the AA floor the three prose tiers hold. It stays: a disabled control read at prose contrast stops reading as disabled, and the versions of this token that clear 4.5:1 looked wrong beside the enabled rows they sit in. It is not prose and it is not a fourth tier; it is the one place the floor is traded for the state being legible *as a state*. Do not "fix" it, and do not cite it as precedent for another sub-AA value — an audit that finds it has found the exemption, not a bug.
- **Links use the solid accent tier** (`--accent-solid`), never raw `--accent` — the accent identifies interaction, and the solid tier is the only accent variant that clears text contrast on every palette. A selection wash or outline is not link text and derives from `--accent` instead — deriving a surface from the link name lets a text-contrast rule silently govern a background.
## 4. Borders
@@ -144,12 +145,12 @@ Nothing interactive is square. One ladder, assigned monotonically by box height:
| Radius | Maka tier | Astryx tier | Assign to |
|---|---|---|---|
| 6px | control | inner | chips, keycaps, nested inlays, and product-drawn compact controls |
-| 10px | card | element | cards, rows-as-cards, list containers; Astryx `Button`, `Input`, `SegmentedControl` |
-| 12px | container | container | modals, panels, portal surfaces; Astryx `Card`, `Dialog`, `DropdownMenu` |
+| 10px | surface | element | cards, rows-as-cards, list containers; Astryx `Button`, `Input`, `SegmentedControl` |
+| 12px | modal | container | modals, panels, portal surfaces; Astryx `Card`, `Dialog`, `DropdownMenu` |
| 28px | chat | chat | the conversation surface as one shape: user bubble, assistant bubble, composer dock. Sourced from Astryx's `--radius-chat` by `ChatMessageBubble` and `ChatComposer` themselves; product CSS never restates it |
-| full | pill (999px) | full (9999px) | badges, pills, circular controls |
+| pill | pill | full | badges, pills, circular controls |
-- **The Two-Name Rule.** These are one ladder under two vocabularies, and the names never line up: Maka's `control` is Astryx's `inner`, Maka's `card` is Astryx's `element`, Maka's `modal` is Astryx's `container`. Resolve a tier from the box, never from the token name that sounds right. The paired values agree *today* but are independent literals, not aliases — an Astryx upgrade can move one side silently, so a mismatch is a real failure mode rather than an impossibility. The chat rung is the one tier the product does not assign: `ChatMessageBubble` and `ChatComposer` both resolve `--radius-chat` on their own, which is why the bubble and the dock round together, and why setting a bubble radius or a non-default `density` in product code silently breaks the pair. Astryx's `--radius-page` carries the same 28px literal but is a different token with no Maka tier and no product consumer; reaching for it to match the chat surface is inventing a rung.
+- **The Two-Name Rule.** These are one ladder under two vocabularies, and the names never line up: Maka's `control` is Astryx's `inner`, Maka's `surface` is Astryx's `element`, Maka's `modal` is Astryx's `container`, Maka's `pill` is Astryx's `full`. Resolve a tier from the box, never from the token name that sounds right. **The px side is the authority and the Astryx name is an alias to it** — `makaTheme.ts` emits `--radius-element: var(--radius-surface)` and its three siblings, so an upstream rung change cannot move one name out from under the other, and the ladder cannot be half in px and half in rem (which the 16px-root note in that file is the reason to avoid). Both vocabularies are live in product CSS and both are correct to read; they resolve to the same number by construction rather than by coincidence. The chat rung is the one tier the product does not assign: `ChatMessageBubble` and `ChatComposer` both resolve `--radius-chat` on their own, which is why the bubble and the dock round together, and why setting a bubble radius or a non-default `density` in product code silently breaks the pair. Astryx's `--radius-page` carries the same 28px literal but is a different token with no Maka tier and no product consumer; reaching for it to match the chat surface is inventing a rung.
- **The Full-Bleed Rule.** `border-radius: 0` is legal only on true full-bleed rows — an element flush with its container on both sides. Radius and gap move together: if it has breathing room, it has corners.
- **Proportional marks.** Product-drawn icon plates use ratio-owned radius (~25–27% of the box edge), recorded in prose because Stitch accepts only absolute units.
@@ -169,14 +170,17 @@ Use the system UI stack with explicit platform CJK fallbacks; Geist Variable is
**The Four-Pixel Line Rule.** Text line boxes land on the 4px grid. Mono is technical, never decorative.
+**The One Measure Rule.** Line length is a typographic decision, so the app has one reading column and one token for it: `--maka-reading-measure`. The container holds it — a turn, the composer, a report — and no component carries a measure of its own, because a measure inside a component is one its container cannot override. Two measures on one column is what the reader sees as two right edges.
+
## 8. Color Specification
The palette is cool-neutral and quiet; color is generated to spec, not picked by eye.
- **Brand mark** is fixed `#71a8fd`; it identifies Maka and is never the general CTA color.
- **Interaction accent** follows the active palette for focus, selection, and live state; **links and accent-colored text use the solid tier** (§3). Astryx's own semantic components are the exception — `Badge` and `StatusDot` carry fixed literals inherited from the neutral theme and follow neither the palette nor the families below.
-- **Status families** — `--info`, `--success`, `--warning`, `--destructive` — are generated, not picked: one lightness per mode, each hue keeping its own chroma, every member clearing AA. The residual contrast spread within a mode is hue physics — at equal lightness, yellow carries more luminance than blue — and flattening it would abandon the shared-lightness premise that makes them a family. All four are declared in one block per mode, because the time warning sat thirty lines from its siblings is the time it lost its dark override and dropped under AA unnoticed. A louder band at ~90% gamut chroma exists only for 8px status dots: dots must read at a glance, washes must not shout. These are colors; what a state *means* is a separate vocabulary (§9).
-- **Tinted surfaces** (status washes behind rows and banners) derive from the same status hues; hand-rolled `oklch()` status washes at call sites are forbidden — consume the family. The family is `--{status}-wash` (0.08 fill) and `--{status}-wash-border` (0.24, ~3x the fill), every member derived with `oklch(from var(--{status}) ...)` so a status regeneration flows through it. A **strong** tier (0.12 / 0.40) exists for warnings about data destruction or an action the user cannot undo, and for nothing else — it is not the loud option for a notice that wants attention. Palette swatches are not washes: a swatch's job is to show a palette's real colour, so its literals stay. The family is kept complete even where a rung has no consumer yet: a family with holes in it sends the next author back to hand-rolling an alpha, which is the etiology of the fourteen that drifted.
+- **Status families** — `--success`, `--warning`, `--destructive` — are generated, not picked: one lightness per mode, each hue keeping its own chroma, every member clearing AA. These are Astryx's three statuses, and Maka has no fourth: there is **no `--info` colour**. "Info" is a semantic slot, not a hue — a `tone="info"` tile paints with the accent, so an informational surface follows the user's palette instead of introducing a second blue that competes with it. `` is the one place that does not yet: `.astryx-banner.info` re-declares the tint and the text colour **on the banner element itself**, which no ancestor bridge can outrank, so info banners stay upstream's fixed blue. Converging that means overriding a component rule rather than a token, and it is tracked on #3446, not done here. The residual contrast spread within a mode is hue physics — at equal lightness, yellow carries more luminance than red — and flattening it would abandon the shared-lightness premise that makes them a family. All three are declared in one block per mode, because the time warning sat thirty lines from its siblings is the time it lost its dark override and dropped under AA unnoticed. A louder band at ~90% gamut chroma exists only for 8px status dots: dots must read at a glance, tinted surfaces must not shout. These are colors; what a state *means* is a separate vocabulary (§9).
+- **Tinted surfaces** are Astryx's own tokens: `--color-success-muted`, `--color-warning-muted`, `--color-error-muted` and `--color-accent-muted`. Product CSS reads those four names and nothing else; a hand-rolled `oklch(from var(--destructive) …)` at a call site is forbidden. **One rung, no border rung, no strong tier** — the shape upstream ships. All four are `oklch(from var(--{hue}) l c h / 0.24)`, the alpha the neutral theme's own pastels already sit at, so a Banner and a product row painted side by side read as one material. Same weight is not enough: they must be the same **construction**, because an opaque tint does not compose, and a tinted chip on a tinted panel then resolves to its parent's exact pixel and vanishes. The three status rungs are emitted by `makaTheme.ts`; `--color-accent-muted` needs the unlayered bridge in `maka-tokens.css` instead, because `maka.css` re-declares the accent pair at component level and a root rule in the same layer would lose. A border around a tinted surface is `--border`: the fill already states the status, and a second tinted edge is §4's One Means Rule. A tinted border on an **un**tinted surface is a different case and does read a `-muted` token — the stat tile's outline tones and the tool-output edges are the live examples. The Maka `--{status}-wash` / `-wash-border` vocabulary this replaced was a second authority for the same surface — its two tiers and four rungs each grew a call site that disagreed with the rung beside it. Palette swatches are not tinted surfaces: a swatch's job is to show a palette's real colour, so its literals stay.
+- **The solid tier is deliberately not converged.** `--success` / `--warning` / `--destructive` remain Maka's names for status ink, dots and solid fills, while Astryx calls the same idea `--color-success` / `--color-warning` / `--color-error`. Only the tinted tier has one authority today. Converging the solid tier means moving status text, status dots and status buttons at once; it is a change of its own, not an omission from this one.
- **Identity colors** (avatars, channel marks) live in one 4.2–4.8:1 contrast band; desaturation for muted states happens at constant OKLab lightness.
**The Signal, Not Texture Rule.** Accent communicates action or state. Never use it as a background flood, gradient, glow, or substitute for hierarchy.
@@ -188,7 +192,7 @@ Use Astryx primitives as the default seam. New work composes product meaning thr
- **Controls:** Maka uses a 20/24/28/32/36/40px height ruler with 32px as the default; Astryx owns the 28/32/36px variants. Hover is restrained; press may use `scale(0.98)`; keyboard focus is always visible. At most one inverted (filled) element per control. Hover washes come in exactly two lanes: product rows and controls take `--state-hover-bg`; chrome that must stay in lockstep with Astryx internals takes `--color-overlay-hover`. Hand-mixed hover alphas are drift.
- **Fields:** labels, descriptions, and validation belong to the field primitive; input focus belongs to its control. Keep disabled reasons discoverable through the owning control's tooltip; do not rebuild field chrome around a bare input.
- **Badges and status:** Badge is 20px high and pill-shaped. Choose variants by meaning, not hue.
-- **Status vocabulary:** what a state means is named once, in `packages/ui/src/status-vocabulary.ts`, and every status dot resolves its color through it — a surface never maps its own domain state onto a color. The semantics are `success` (proven healthy), `active` (the system is working), `attention` (waiting on a person), `error` (broken now), `neutral` (a settled fact); collapsing `active` and `attention` is the mistake that vocabulary exists to prevent. It deliberately has no `info` — two callers meant opposite things by it — but that is a statement about *dot semantics*, not about the `--info` color, which is live. Note that Astryx's `Badge` and `StatusDot` render fixed inherited literals, so an Astryx `info` pill and Maka's `--info` are two unrelated blues that merely look alike; a dot's color does not come from the family in §8.
+- **Status vocabulary:** what a state means is named once, in `packages/ui/src/status-vocabulary.ts`, and every status dot resolves its color through it — a surface never maps its own domain state onto a color. The semantics are `success` (proven healthy), `active` (the system is working), `attention` (waiting on a person), `error` (broken now), `neutral` (a settled fact); collapsing `active` and `attention` is the mistake that vocabulary exists to prevent. It deliberately has no `info` — two callers meant opposite things by it. Note that Astryx's `Badge` and `StatusDot` render fixed inherited literals, so an Astryx `info` pill does not follow the palette; a dot's color does not come from the family in §8.
- **Counters:** a count is one step smaller and quieter than its label (supporting role, muted ink, `tabular-nums`) and follows its parent's active state back to full ink. Never bolder than the label it counts.
- **Scrollbars:** one app-wide recipe — a 6px pill (10px hit area, 2px transparent inset), thumb at `--border-strong`, one step darker on hover, no painted track. Surfaces may hide their own bars; none may restyle them.
- **Cards:** Astryx Card uses container radius, 12px default padding, and no resting elevation. Astryx components own their geometry.
@@ -243,7 +247,7 @@ Three mutually exclusive forms, chosen by structural predictability — never by
### Don't (the forbidden list — some items are contract-tested, the rest are review-blocked):
-- **Don't** write a bare `oklch()` status color at a call site, or hand-roll a status wash — consume the families (§8).
+- **Don't** write a bare `oklch()` status color at a call site, or hand-roll a status tint — consume the families (§8).
- **Don't** use `border-radius: 0` off a full-bleed row (§6).
- **Don't** put an illustration in an empty-state icon slot, add anything past tier 3's single action, or ship a state attribute with no visible feedback (§10).
- **Don't** hardcode `background: white` or any literal surface color — resolve a ladder tier (§2), or `--surface-paper` when the content's own contrast is not ours to control (foreign documents, QR codes) and inverting it would break the content rather than restyle it.
@@ -253,5 +257,5 @@ Three mutually exclusive forms, chosen by structural predictability — never by
- **Don't** use generic AI gradients, glowing borders, sparkle, decorative "thinking," or default glassmorphism.
- **Don't** personify the agent through mascots, fake emotion, excessive avatars, or chat ornament; the optional user-supplied pet is the only exception.
- **Don't** turn every region into a card or every status into a colored pill.
-- **Don't** introduce another accent, spacing ruler, radius tier, icon system, text axis, or parallel component path.
+- **Don't** introduce another accent, spacing ruler, radius tier, icon system, text axis, reading measure, or parallel component path (§7).
- **Don't** copy primitive internals, progress, versions, palette inventories, or surface inventories into this document.
diff --git a/LICENSE b/LICENSE
index 2d8fc74e5d..6371a078e8 100644
--- a/LICENSE
+++ b/LICENSE
@@ -230,13 +230,13 @@ Astryx
Source: https://github.com/facebook/astryx
ChatReasoning revision: c9fe4379e9959b9ba5eeff56def34c752223450e
-Theme revision: 110987b4505dc44119b94bed53d92b9840088a61
+Theme revision: ea888ef33283ea44e37817d36bc8e84a0568bc30
Copyright (c) 2026 Meta Platforms, Inc.
License: MIT
Maka includes an ejected adaptation of Astryx ChatReasoning v0.1.9 and generated
-theme artifacts based on Astryx theme-neutral v0.4.5. Maka also redistributes
-`patches/@astryxdesign+core+0.4.5.patch`, which modifies the MIT-licensed Astryx
+theme artifacts based on Astryx theme-neutral v0.5.2. Maka also redistributes
+`patches/@astryxdesign+core+0.5.2.patch`, which modifies the MIT-licensed Astryx
core package. The following MIT License applies to that material:
MIT License
@@ -261,6 +261,53 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+Sigstore dependency patch
+
+Source: https://www.npmjs.com/package/@sigstore/core/v/4.0.1
+Repository: https://github.com/sigstore/sigstore-js/tree/main/packages/core
+Version: 4.0.1
+Dependency patch: patches/@sigstore+core+4.0.1.patch
+Copyright 2023 The Sigstore Authors
+License: Apache-2.0
+
+Maka redistributes a source patch that selects the signature digest in
+@sigstore/core. The patch modifies Apache-2.0 source and is covered by the
+Apache License, Version 2.0 reproduced above.
+
+TUF metadata models dependency patch
+
+Source: https://www.npmjs.com/package/@tufjs/models/v/5.0.0
+Repository: https://github.com/theupdateframework/tuf-js/tree/main/packages/models
+Version: 5.0.0
+Dependency patch: patches/@tufjs+models+5.0.0.patch
+Copyright (c) 2022 GitHub and the TUF Contributors
+License: MIT
+
+Maka redistributes a source patch that selects the signature digest in
+@tufjs/models. The following MIT License applies to that material:
+
+MIT License
+
+Copyright (c) 2022 GitHub and the TUF Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
node-pty dependency patch
Source: https://www.npmjs.com/package/node-pty/v/1.2.0-beta.15
@@ -450,14 +497,18 @@ License: MIT
packages/core/src/model-metadata.generated.ts and
packages/runtime/src/telemetry/model-pricing.generated.ts are generated from the
-models.dev catalog by scripts/sync-model-metadata.mjs and are redistributed with
-Maka. The exact selected input projection is stored at
+models.dev catalog by scripts/sync-model-metadata.mjs during installation or
+build and are redistributed with compiled Maka packages, but are not tracked as
+source. The exact selected input projection is stored at
scripts/model-metadata/models-dev-api.snapshot.json. Its origin record either
binds a refresh to the downloaded response digest or identifies the exact Maka
commit whose generated outputs seeded the migration snapshot. The snapshot
-also carries a digest of its normalized projection; the generated-file headers
-bind each output to that digest. They copy and transform selected model and
-provider metadata from that catalog, including upstream display names and
+is the sole build input for a particular repository revision and release;
+models.dev remains the upstream source used by an explicit refresh. Normal
+installation and build paths do not fetch the latest catalog implicitly. The
+snapshot also carries a digest of its normalized projection; the generated-file
+headers bind each output to that digest. They copy and transform selected model
+and provider metadata from that catalog, including upstream display names and
descriptive text, lifecycle status values, reasoning effort options, context
and output limits, modalities, and prices. Which providers are kept, which
fields are kept, and the order they appear in are decided by the generator. The
diff --git a/README.md b/README.md
index 58fc692e35..93d6154eb4 100644
--- a/README.md
+++ b/README.md
@@ -25,14 +25,11 @@
-
-
-
-
+
@@ -43,13 +40,18 @@
machine, through one Runtime Host.
+
+ 
+ Daily builds from main for developers and testers. Not an ASF release, not intended for production use.
+
+

> [!NOTE]
> Apache Maka (Incubating) is an effort undergoing incubation at The Apache Software Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is required of all newly accepted projects until a further review indicates that the infrastructure, communications, and decision-making process have stabilized in a manner consistent with other successful ASF projects. While incubation status is not necessarily a reflection of the completeness or stability of the code, it does indicate that the project has yet to be fully endorsed by the ASF. [DISCLAIMER-WIP](./DISCLAIMER-WIP) records the issues the project is currently aware of.
> [!IMPORTANT]
-> Maka is under active development. The macOS Apple Silicon desktop build is an early public release; data formats, CLI commands, and experimental capabilities may still change.
+> Maka is under active development. Data formats, CLI commands, and experimental capabilities may still change.
## Why Maka
@@ -99,7 +101,7 @@ Apache Maka has not made an Apache release yet. Everything currently published f
Once Apache releases exist, the official release is the source release published by the ASF and approved by the podling PPMC and the Incubator PMC. A package built from that source and distributed elsewhere, for example through a package registry or as a Desktop installer, is a convenience artifact rather than the release itself, and it is valid only when it is built from an approved source release. [`.github/ASF_SOURCE_RELEASE.md`](./.github/ASF_SOURCE_RELEASE.md) holds the candidate contract, signing path, and verification steps.
-Until an approved source release exists, this README recommends no prebuilt download. Build and run Maka from source as described below. Desktop currently targets Apple Silicon Macs (`arm64`). Intel Macs and Linux are not supported yet. [Windows](docs/windows-support.md) is an unsigned preview, not a supported release tier.
+[Desktop Nightly](https://github.com/apache/maka/releases) is built daily from `main` for developers and testers. Choose the newest **Maka Desktop Nightly** prerelease; after installation, the app updates automatically on the Nightly channel. It is not an ASF release and is not intended for production use. Desktop currently targets Apple Silicon Macs (`arm64`). Intel Macs and Linux are not supported yet. [Windows](docs/windows-support.md) is an unsigned preview, not a supported release tier.
### Requirements
@@ -123,6 +125,15 @@ npm run dev
npm run dev:full
```
+Direct Peer and Peer Mesh development additionally requires Rust stable 1.98 or newer and the
+platform linker (Xcode Command Line Tools on macOS, MSVC Build Tools on Windows). Use the
+peer-enabled entry point so the native addon is built before Desktop starts:
+
+```sh
+npm run dev:peer # HMR
+npm run dev:full:peer # full build
+```
+
If dependencies were installed with `ELECTRON_SKIP_BINARY_DOWNLOAD=1`, install the Electron platform binary before starting:
```sh
@@ -190,17 +201,20 @@ Start with [ARCHITECTURE.md](./ARCHITECTURE.md). It provides the system map, cod
## Repository layout
```text
-apps/desktop/ Electron main / preload / React renderer
-
-packages/core/ Pure contracts for Sessions, Events, Permissions, and Connections
-packages/storage/ SQLite operational state, configuration, and payload stores
-packages/runtime/ AgentRun, model adapters, tools, context, and recovery
-packages/eval/ Experiment cells, attempts, results, and executor/subject adapters
-packages/cli/ TUI and non-interactive CLI
-packages/ui/ Shared conversation, Markdown, Artifact, and UI primitives
-
-docs/ Architecture, product, security, privacy, and test contracts
-scripts/ Build hygiene, visual checks, smoke tests, and release helpers
+apps/desktop/ Electron main / preload / React renderer
+
+packages/core/ Pure contracts for Sessions, Events, Permissions, and Connections
+packages/storage/ SQLite operational state, configuration, and payload stores
+packages/mcp/ Provider-neutral Model Context Protocol client integration
+packages/runtime/ AgentRun, model adapters, tools, context, and recovery
+packages/runtime-host/ Single-owner Runtime Host lifecycle, protocol, and client bootstrap
+packages/eval/ Experiment cells, attempts, results, and executor/subject adapters
+packages/computer-use/ Computer-use backend selection, host lifecycle, and protocol adapters
+packages/cli/ TUI and non-interactive CLI
+packages/ui/ Shared conversation, Markdown, Artifact, and UI primitives
+
+docs/ Architecture, product, security, privacy, and test contracts
+scripts/ Build hygiene, visual checks, smoke tests, and release helpers
```
## Local data and recovery
@@ -239,16 +253,16 @@ npm run check:release
Run one workspace in isolation:
```sh
-npm --workspace @maka/runtime test
-npm --workspace @maka/eval test
-npm --workspace @maka/desktop test
+npm --workspace @maka/runtime run test:dist
+npm --workspace @maka/eval run test:dist
+npm --workspace @maka/desktop run test:dist
```
Use `refresh:model-metadata` to fetch the current catalog from models.dev, update the committed snapshot, and regenerate the derived TypeScript files. A refresh fails closed when any committed model, capability, provider override, or pricing field disappears; after reviewing an intentional upstream removal, acknowledge it with `npm run refresh:model-metadata -- --accept-upstream-removals`. `sync:model-metadata` is intentionally offline: it only regenerates those files from the committed snapshot. Keep access-path-specific overrides in `model-metadata.ts`; do not edit the generated files by hand.
```sh
npm run refresh:model-metadata
-npm --workspace @maka/core test
+npm --workspace @maka/core run test:dist
```
Desktop real-window and visual verification:
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 21e363d77a..6c64fb51ff 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -25,14 +25,11 @@
-
-
-
-
+
@@ -41,13 +38,18 @@
Maka 在沙箱边界下阅读项目、执行工具,并把模型消息和工具调用保存为可恢复的运行事实——数据在本机,执行走同一个 Runtime Host。
+
+ 
+ 每天从 main 构建,面向开发者和测试者。不是 ASF release,也不适合生产使用。
+
+

> [!NOTE]
> Apache Maka (Incubating) 是一个正在 Apache 软件基金会(ASF)孵化的项目,由 Apache Incubator PMC 提供 sponsor。所有新接受的项目都必须经过孵化,直到进一步审查表明其基础设施、沟通方式和决策流程已经稳定到与其他成功的 ASF 项目一致的程度。孵化状态并不必然反映代码的完成度或稳定性,但它确实表明该项目尚未得到 ASF 的完全认可。项目当前已知的问题记录在 [DISCLAIMER-WIP](./DISCLAIMER-WIP)(以英文原文为准)。
> [!IMPORTANT]
-> Maka 仍在活跃开发中。macOS Apple Silicon 桌面版是首个早期公开版本,数据格式、CLI 和实验能力仍可能变化。
+> Maka 仍在活跃开发中。数据格式、CLI 和实验能力仍可能变化。
## 为什么是 Maka
@@ -97,7 +99,7 @@ Apache Maka 目前还没有发布过 Apache release。当前从本仓库或包
在 Apache release 出现之后,官方 release 指的是由 ASF 发布、并经 podling PPMC 和 Incubator PMC 批准的源码 release。由该源码构建并通过其他渠道分发的包,例如包管理器中的包或 Desktop 安装程序,属于 convenience artifact,本身不是 release,并且只有在由获批源码 release 构建时才有效。候选契约、签名路径和验包步骤见 [`.github/ASF_SOURCE_RELEASE.md`](./.github/ASF_SOURCE_RELEASE.md)。
-在获批源码 release 出现之前,本 README 不推荐任何预构建下载,请按下文从源码构建并运行 Maka。Desktop 目前面向 Apple Silicon Mac(`arm64`)。暂不支持 Intel Mac 和 Linux。[Windows](docs/windows-support.md) 是未签名预览,不是正式支持的发布层级。
+[Desktop Nightly](https://github.com/apache/maka/releases) 面向开发者和测试者,每天从 `main` 构建。请选择最新的 **Maka Desktop Nightly** prerelease;安装后,应用会在 Nightly 渠道自动更新。它不是 ASF release,不适合生产使用。Desktop 目前面向 Apple Silicon Mac(`arm64`)。暂不支持 Intel Mac 和 Linux。[Windows](docs/windows-support.md) 是未签名预览,不是正式支持的发布层级。
### 环境要求
@@ -121,6 +123,15 @@ npm run dev
npm run dev:full
```
+开发 Direct Peer 和 Peer Mesh 还需要 Rust stable 1.98 或更高版本及平台 linker
+(macOS 使用 Xcode Command Line Tools,Windows 使用 MSVC Build Tools)。使用 Peer 开发入口,
+Desktop 会在启动前构建原生 addon:
+
+```sh
+npm run dev:peer # HMR
+npm run dev:full:peer # 完整构建
+```
+
如果安装时设置过 `ELECTRON_SKIP_BINARY_DOWNLOAD=1`,启动前需要补装 Electron 平台二进制:
```sh
@@ -187,17 +198,20 @@ Experiment → Cells → Attempts → Results
## 仓库结构
```text
-apps/desktop/ Electron main / preload / React renderer
-
-packages/core/ Session、Event、Permission、Connection 等纯 contracts
-packages/storage/ SQLite 运行状态、配置与 payload stores
-packages/runtime/ AgentRun、模型适配、工具、上下文和恢复
-packages/eval/ Experiment cell、attempt、result 与 executor/subject adapter
-packages/cli/ TUI 和非交互 CLI
-packages/ui/ 共享对话、Markdown、Artifact 与 UI primitives
-
-docs/ 架构、产品、安全、隐私和测试契约
-scripts/ Build hygiene、视觉检查、smoke 和 release helpers
+apps/desktop/ Electron main / preload / React renderer
+
+packages/core/ Session、Event、Permission、Connection 等纯 contracts
+packages/storage/ SQLite 运行状态、配置与 payload stores
+packages/mcp/ 与提供商无关的 Model Context Protocol 客户端集成
+packages/runtime/ AgentRun、模型适配、工具、上下文和恢复
+packages/runtime-host/ 单一所有者的 Runtime Host 生命周期、协议和客户端启动
+packages/eval/ Experiment cell、attempt、result 与 executor/subject adapter
+packages/computer-use/ Computer Use 后端选择、Host 生命周期和协议适配
+packages/cli/ TUI 和非交互 CLI
+packages/ui/ 共享对话、Markdown、Artifact 与 UI primitives
+
+docs/ 架构、产品、安全、隐私和测试契约
+scripts/ Build hygiene、视觉检查、smoke 和 release helpers
```
## 本地数据与恢复
@@ -236,16 +250,16 @@ npm run check:release
针对单个 workspace:
```sh
-npm --workspace @maka/runtime test
-npm --workspace @maka/eval test
-npm --workspace @maka/desktop test
+npm --workspace @maka/runtime run test:dist
+npm --workspace @maka/eval run test:dist
+npm --workspace @maka/desktop run test:dist
```
用 `refresh:model-metadata` 从 models.dev 获取当前目录、更新仓库内快照,并重新生成派生的 TypeScript 文件。已提交的模型、能力、provider override 或 pricing 字段消失时,refresh 会 fail closed;审查确认上游确实有意删除后,用 `npm run refresh:model-metadata -- --accept-upstream-removals` 显式确认。`sync:model-metadata` 刻意保持离线,只会从已提交快照重新生成这些文件。访问路径特有的 override 写在 `model-metadata.ts`,不要手动修改生成文件。
```sh
npm run refresh:model-metadata
-npm --workspace @maka/core test
+npm --workspace @maka/core run test:dist
```
Desktop 的真实窗口与视觉验证:
diff --git a/SECURITY.md b/SECURITY.md
index 22021811da..d5c1c575ca 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -80,7 +80,7 @@ and they are NOT equally load-bearing.
user's OS account: filesystem, network, OS permissions
(Keychain / Microphone / Screen recording).
-### 2.2 The boundary: the OS user account
+### 2.2 OS enforcement boundaries
**The only enforcement boundary against an adversarial LLM is the
operating system.** Nothing inside the agent process constitutes
@@ -94,11 +94,28 @@ because they are useful UX safety nets — they catch accidental
output and slow down adversarial output enough for a human to
notice — but we do not ship them as guarantees.
-Maka does not run tools in a separate process or container by default. The
-runtime exposes a macOS Seatbelt command transformer for restricted profiles,
-but current product compositions do not yet route command execution through
-it, so it is not a product boundary today. Externally isolated runtimes may
-supply their own boundary. See
+The OS user account is Maka's outer trust envelope. Inside that envelope,
+Runtime Host also routes selected agent tool effects through OS-enforced child
+process sandboxes when the active session has a restricted managed
+`ExecutionBoundary`. On macOS and Linux, non-PTY Bash commands and the
+filesystem worker run through Seatbelt and bubblewrap respectively. On
+Windows, the AppContainer boundary currently covers the purpose-built
+filesystem worker only; arbitrary-shell Bash is unavailable and fails closed
+when the active profile requires a command sandbox.
+
+This is not universal tool containment. Bypass boundaries and unrestricted,
+disabled, or external profiles do not add a Maka-managed local sandbox;
+external environments may supply their own isolation. Managed PTY Bash is
+refused when the active profile requires sandboxing. Client-launched
+integrated terminals are host PTYs outside the managed agent execution
+boundary, and runtime or attachment resource reads do not execute through the
+local filesystem worker. The standard `workspace-write` profile permits the
+workspace roots and the configured temporary roots.
+
+Seatbelt, bubblewrap, and AppContainer are OS enforcement mechanisms. The
+in-process checks that decide whether to request or invoke them remain
+heuristics, not containment. The exact product coverage and fail-closed
+selection rules are documented in
[`packages/runtime/src/sandbox/README.md`](./packages/runtime/src/sandbox/README.md).
### 2.3 Boundaries we DO treat as load-bearing
@@ -106,7 +123,14 @@ supply their own boundary. See
1. **OS user account.** Tools run with the user's privileges. The
user is expected to run Maka as a non-admin account on systems
where that matters.
-2. **Credential-at-rest boundaries.** The provider credential store
+2. **Restricted managed tool sandboxes.** For the product surfaces listed in
+ §2.2, the active session `ExecutionBoundary` is compiled into a per-command
+ Seatbelt, bubblewrap, or AppContainer policy. Required enforcement fails
+ closed rather than silently retrying on the host. This boundary is limited
+ to the documented tool and platform matrix; it does not include bypass,
+ unrestricted, disabled, external, managed PTY, or integrated-terminal
+ execution.
+3. **Credential-at-rest boundaries.** The provider credential store
writes `credentials.json` as versioned plaintext JSON under the
user's workspace directory. Its load-bearing boundary is the OS
user account plus filesystem controls: directory mode 0o700,
@@ -119,21 +143,21 @@ supply their own boundary. See
this boundary anymore. Pre-existing safeStorage-encrypted credential
or token files are not imported; users with only those copies must
re-authenticate.
-3. **Renderer process sandbox + preload IPC bridge.** The
+4. **Renderer process sandbox + preload IPC bridge.** The
renderer cannot reach files, network, or shell directly. Every
IPC handler in `apps/desktop/src/main/main.ts` is the trust
boundary between renderer-controlled input and main-process
action. Renderer code is treated as semi-trusted: it can read
masked / sanitized data, but cleartext secrets never cross the
boundary in the main-to-renderer direction (see §4).
-4. **Settings sensitive masking.** Tokens, API keys, and proxy
+5. **Settings sensitive masking.** Tokens, API keys, and proxy
passwords are masked at the IPC store boundary
(`maskAppSettings` in `apps/desktop/src/main/settings-ipc-helpers.ts`).
Re-submitting the mask sentinel `••••••` is interpreted as
"keep current" by the merge logic; an empty string is
interpreted as an explicit clear. The Tavily API key follows the
same boundary.
-5. **Network egress through user-configured proxies.** The
+6. **Network egress through user-configured proxies.** The
`network.proxy` settings drive Electron's session proxy. Tools
that bypass `proxiedFetch` (Tavily lives in main, uses
standard fetch) are individually audited.
@@ -156,8 +180,10 @@ are welcome as ordinary issues, not security advisories.
URLs out of agent tool results before they reach a renderer
``.
4. **`PermissionMode.ask`** as default. Mode names are UX controls,
- not security boundaries; even permissive modes remain subject to
- the current policy table and OS isolation boundary.
+ not security boundaries. `ask` and `explore` establish restricted
+ managed profiles, while `bypass` explicitly does not promise a
+ Maka-managed local sandbox; the active `ExecutionBoundary` is the
+ execution authority.
5. **Modal-lifecycle contract test.** Catches the React
`useEffect`-before-`if (!open) return null` pattern that can
violate React hook ordering. Static analysis only.
@@ -204,6 +230,10 @@ boundary in §2.3 was crossed. Examples:
result envelopes, log lines).
- A tool intended to be permission-gated bypasses the
PermissionEngine.
+- A non-PTY Bash command or local-path filesystem operation under a
+ restricted managed boundary escapes its effective Seatbelt,
+ bubblewrap, or supported AppContainer policy, or silently falls
+ back to host execution when required enforcement is unavailable.
Reports against the §2.4 heuristics are out of scope as security
advisories. They are welcome as ordinary issues or pull requests.
diff --git a/apps/desktop/assets/app-icons/amber-crt.png b/apps/desktop/assets/app-icons/amber-crt.png
new file mode 100644
index 0000000000..fc761be52a
Binary files /dev/null and b/apps/desktop/assets/app-icons/amber-crt.png differ
diff --git a/apps/desktop/assets/app-icons/amber.png b/apps/desktop/assets/app-icons/amber.png
new file mode 100644
index 0000000000..35eeb088ef
Binary files /dev/null and b/apps/desktop/assets/app-icons/amber.png differ
diff --git a/apps/desktop/assets/app-icons/carbon.png b/apps/desktop/assets/app-icons/carbon.png
new file mode 100644
index 0000000000..118a8f031e
Binary files /dev/null and b/apps/desktop/assets/app-icons/carbon.png differ
diff --git a/apps/desktop/assets/app-icons/chrome.png b/apps/desktop/assets/app-icons/chrome.png
new file mode 100644
index 0000000000..bf7cef4e58
Binary files /dev/null and b/apps/desktop/assets/app-icons/chrome.png differ
diff --git a/apps/desktop/assets/app-icons/clay.png b/apps/desktop/assets/app-icons/clay.png
new file mode 100644
index 0000000000..fb47d8248e
Binary files /dev/null and b/apps/desktop/assets/app-icons/clay.png differ
diff --git a/apps/desktop/assets/app-icons/desert.png b/apps/desktop/assets/app-icons/desert.png
new file mode 100644
index 0000000000..e8b99b49cf
Binary files /dev/null and b/apps/desktop/assets/app-icons/desert.png differ
diff --git a/apps/desktop/assets/app-icons/dust.png b/apps/desktop/assets/app-icons/dust.png
new file mode 100644
index 0000000000..4d7f1359fe
Binary files /dev/null and b/apps/desktop/assets/app-icons/dust.png differ
diff --git a/apps/desktop/assets/app-icons/fog.png b/apps/desktop/assets/app-icons/fog.png
new file mode 100644
index 0000000000..1b08085679
Binary files /dev/null and b/apps/desktop/assets/app-icons/fog.png differ
diff --git a/apps/desktop/assets/app-icons/glacier.png b/apps/desktop/assets/app-icons/glacier.png
new file mode 100644
index 0000000000..b8cc3cf972
Binary files /dev/null and b/apps/desktop/assets/app-icons/glacier.png differ
diff --git a/apps/desktop/assets/app-icons/gold.png b/apps/desktop/assets/app-icons/gold.png
new file mode 100644
index 0000000000..23fdf4cd33
Binary files /dev/null and b/apps/desktop/assets/app-icons/gold.png differ
diff --git a/apps/desktop/assets/app-icons/hazard.png b/apps/desktop/assets/app-icons/hazard.png
new file mode 100644
index 0000000000..2945dec384
Binary files /dev/null and b/apps/desktop/assets/app-icons/hazard.png differ
diff --git a/apps/desktop/assets/app-icons/magenta.png b/apps/desktop/assets/app-icons/magenta.png
new file mode 100644
index 0000000000..99e9896af6
Binary files /dev/null and b/apps/desktop/assets/app-icons/magenta.png differ
diff --git a/apps/desktop/assets/app-icons/matrix.png b/apps/desktop/assets/app-icons/matrix.png
new file mode 100644
index 0000000000..26f4ea2e02
Binary files /dev/null and b/apps/desktop/assets/app-icons/matrix.png differ
diff --git a/apps/desktop/assets/app-icons/midnight.png b/apps/desktop/assets/app-icons/midnight.png
new file mode 100644
index 0000000000..d4cdafa62b
Binary files /dev/null and b/apps/desktop/assets/app-icons/midnight.png differ
diff --git a/apps/desktop/assets/app-icons/mono-black.png b/apps/desktop/assets/app-icons/mono-black.png
new file mode 100644
index 0000000000..f42bf4ca67
Binary files /dev/null and b/apps/desktop/assets/app-icons/mono-black.png differ
diff --git a/apps/desktop/assets/app-icons/mono-white.png b/apps/desktop/assets/app-icons/mono-white.png
new file mode 100644
index 0000000000..bc572d679d
Binary files /dev/null and b/apps/desktop/assets/app-icons/mono-white.png differ
diff --git a/apps/desktop/assets/app-icons/moss.png b/apps/desktop/assets/app-icons/moss.png
new file mode 100644
index 0000000000..897c9159dd
Binary files /dev/null and b/apps/desktop/assets/app-icons/moss.png differ
diff --git a/apps/desktop/assets/app-icons/neon-cyan.png b/apps/desktop/assets/app-icons/neon-cyan.png
new file mode 100644
index 0000000000..e847e89a7a
Binary files /dev/null and b/apps/desktop/assets/app-icons/neon-cyan.png differ
diff --git a/apps/desktop/assets/app-icons/obsidian.png b/apps/desktop/assets/app-icons/obsidian.png
new file mode 100644
index 0000000000..c087c465bb
Binary files /dev/null and b/apps/desktop/assets/app-icons/obsidian.png differ
diff --git a/apps/desktop/assets/app-icons/ocean.png b/apps/desktop/assets/app-icons/ocean.png
new file mode 100644
index 0000000000..61775aa3f1
Binary files /dev/null and b/apps/desktop/assets/app-icons/ocean.png differ
diff --git a/apps/desktop/assets/app-icons/sage.png b/apps/desktop/assets/app-icons/sage.png
new file mode 100644
index 0000000000..b0c453d8f8
Binary files /dev/null and b/apps/desktop/assets/app-icons/sage.png differ
diff --git a/apps/desktop/assets/app-icons/slate.png b/apps/desktop/assets/app-icons/slate.png
new file mode 100644
index 0000000000..12ac8d73aa
Binary files /dev/null and b/apps/desktop/assets/app-icons/slate.png differ
diff --git a/apps/desktop/assets/app-icons/sunset.png b/apps/desktop/assets/app-icons/sunset.png
new file mode 100644
index 0000000000..82ceb8811f
Binary files /dev/null and b/apps/desktop/assets/app-icons/sunset.png differ
diff --git a/apps/desktop/assets/app-icons/terracotta.png b/apps/desktop/assets/app-icons/terracotta.png
new file mode 100644
index 0000000000..d9cd10f8e2
Binary files /dev/null and b/apps/desktop/assets/app-icons/terracotta.png differ
diff --git a/apps/desktop/build/background.png b/apps/desktop/build/background.png
new file mode 100644
index 0000000000..46e4393408
Binary files /dev/null and b/apps/desktop/build/background.png differ
diff --git a/apps/desktop/build/background@2x.png b/apps/desktop/build/background@2x.png
new file mode 100644
index 0000000000..ebfb6909d9
Binary files /dev/null and b/apps/desktop/build/background@2x.png differ
diff --git a/apps/desktop/e2e/accessibility-coverage.spec.ts b/apps/desktop/e2e/accessibility-coverage.spec.ts
index 8279c7c8e8..322ed8d8fc 100644
--- a/apps/desktop/e2e/accessibility-coverage.spec.ts
+++ b/apps/desktop/e2e/accessibility-coverage.spec.ts
@@ -17,7 +17,8 @@
* under the License.
*/
-import type { CDPSession, Page } from '@playwright/test';
+import { FAKE_HOLD_OPEN_PROMPT } from '@maka/runtime/test-only/fake-backend';
+import type { CDPSession, Locator, Page } from '@playwright/test';
import { expect, test, COMPOSER_INPUT } from './fixtures';
import { auditAxTree } from '../../../scripts/ax-tree-audit.mjs';
import { groupedNav } from '../src/renderer/settings/settings-nav';
@@ -40,6 +41,29 @@ async function openSettings(page: Page): Promise {
await expect(page.getByRole('main', { name: '设置内容' })).toBeVisible();
}
+async function tabTo(page: Page, target: Locator, label: string, limit = 30): Promise {
+ for (let index = 0; index < limit; index += 1) {
+ if (await target.evaluate((element) => element === document.activeElement)) return;
+ await page.keyboard.press('Tab');
+ }
+ expect(
+ await target.evaluate((element) => element === document.activeElement),
+ `${label} is not reachable within ${limit} Tab presses`,
+ ).toBe(true);
+}
+
+async function enterMainFromSkipLink(page: Page): Promise {
+ await page.evaluate(() => {
+ document.body.tabIndex = -1;
+ document.body.focus();
+ });
+ const skipLink = page.getByRole('link', { name: '跳到主要内容' });
+ await tabTo(page, skipLink, 'skip link', 10);
+ await page.keyboard.press('Enter');
+ await expect(page.getByRole('main')).toBeFocused();
+ await page.evaluate(() => document.body.removeAttribute('tabindex'));
+}
+
test('every settings page exposes named actionable controls', async ({ window: page }) => {
await openSettings(page);
const cdp = await page.context().newCDPSession(page);
@@ -83,25 +107,25 @@ test('module pages and global overlays expose named actionable controls', async
const extensionsNavigation = page.getByRole('navigation', { name: /扩展内容/ });
await expect(
extensionsNavigation.getByRole('button', { name: '技能', exact: true }),
- ).toHaveAttribute('aria-current', 'page');
+ ).toHaveAttribute('aria-current', 'true');
await assertAxHealth(cdp, 'extensions/skills');
const mcpButton = extensionsNavigation.getByRole('button', { name: 'MCP', exact: true });
await mcpButton.click();
- await expect(mcpButton).toHaveAttribute('aria-current', 'page');
+ await expect(mcpButton).toHaveAttribute('aria-current', 'true');
await assertAxHealth(cdp, 'extensions/mcp');
await navigation.getByRole('button', { name: /定时任务/ }).click();
const automationsNavigation = page.getByRole('navigation', { name: /定时任务内容/ });
await expect(
automationsNavigation.getByRole('button', { name: '定时任务', exact: true }),
- ).toHaveAttribute('aria-current', 'page');
+ ).toHaveAttribute('aria-current', 'true');
await assertAxHealth(cdp, 'automations/scheduled-tasks');
const dailyReviewButton = automationsNavigation.getByRole('button', {
name: '每日回顾',
exact: true,
});
await dailyReviewButton.click();
- await expect(dailyReviewButton).toHaveAttribute('aria-current', 'page');
+ await expect(dailyReviewButton).toHaveAttribute('aria-current', 'true');
await assertAxHealth(cdp, 'automations/daily-review');
await page.keyboard.press('Shift+Slash');
@@ -118,6 +142,104 @@ test('module pages and global overlays expose named actionable controls', async
await page.keyboard.press('Escape');
});
+test('data-backed conversation exposes ordered todos and keyboard access to tools, models, and Graph', async ({
+ accessibilityNarrativeWindow: page,
+}) => {
+ const cdp = await page.context().newCDPSession(page);
+ await expect(page.getByRole('region', { name: /对话:/ })).toBeVisible();
+ const todoRegion = page.getByRole('region', { name: '任务待办' });
+ await expect(todoRegion).toBeVisible();
+ await expect(todoRegion.getByRole('listitem')).toHaveText([
+ '补齐桌面端无障碍覆盖',
+ '核对模型选择器的键盘路径',
+ '确认工具结果可以展开阅读',
+ ]);
+ await assertAxHealth(cdp, 'conversation/data-backed');
+
+ await expect(page.getByRole('main')).toHaveCount(1);
+ await enterMainFromSkipLink(page);
+
+ const toolCall = page.getByRole('button', { name: /^检查测试状态/ });
+ await tabTo(page, toolCall, 'tool result');
+ await page.keyboard.press('Enter');
+ await expect(toolCall).toHaveAttribute('aria-expanded', 'true');
+ await expect(page.locator('[data-slot="tool-output"]')).toContainText('core 41 passing');
+ await assertAxHealth(cdp, 'conversation/tool-result-expanded');
+ await page.keyboard.press('Enter');
+ await expect(toolCall).toHaveAttribute('aria-expanded', 'false');
+
+ const modelSwitcher = page.getByRole('button', { name: '切换当前任务模型' });
+ await tabTo(page, modelSwitcher, 'model picker');
+ await page.keyboard.press('Enter');
+ await expect(page.getByRole('menuitem', { name: /glm-5\.1/ })).toBeVisible();
+ await assertAxHealth(cdp, 'conversation/model-picker');
+ const availableModel = page.getByRole('menuitem', { name: 'glm-4.5', exact: true });
+ await expect(availableModel).toBeFocused();
+ await page.keyboard.press('Enter');
+ await expect(modelSwitcher).toContainText('glm-4.5');
+
+ const composer = page.locator(COMPOSER_INPUT);
+ await tabTo(page, composer, 'composer', 60);
+ await page.keyboard.insertText('/graph on');
+ const send = page.getByRole('button', { name: '发送' });
+ await tabTo(page, send, 'Send button', 20);
+ await page.keyboard.press('Enter');
+ await expect(page.getByText('Graph Mode 已开启', { exact: true })).toBeVisible();
+ await assertAxHealth(cdp, 'overlay/graph-mode-toast');
+
+ const graphPanel = page.getByRole('region', { name: 'Agent Graph' });
+ await expect(graphPanel).toBeVisible();
+ await expect(graphPanel).toContainText('等待主 Agent 创建 operator…');
+ const collapseGraph = graphPanel.getByRole('button', { name: '收起 Agent Graph' });
+ await tabTo(page, collapseGraph, 'Graph collapse', 60);
+ await page.keyboard.press('Enter');
+ await expect(
+ graphPanel.getByRole('button', { name: '展开 Agent Graph' }),
+ ).toHaveAttribute('aria-expanded', 'false');
+ await assertAxHealth(cdp, 'conversation/agent-graph-empty');
+});
+
+test('toast and error states expose healthy live regions', async ({ window: page }) => {
+ const cdp = await page.context().newCDPSession(page);
+ const composer = page.locator(COMPOSER_INPUT);
+ await composer.fill('/graph history');
+ await composer.press('Enter');
+ await expect(page.getByText('Graph 历史', { exact: true })).toBeVisible();
+ await assertAxHealth(cdp, 'overlay/graph-history-toast');
+
+ await page.evaluate(async () => {
+ await window.maka.connections.setDefaultModel(null);
+ await window.maka.settings.updateClient({ workHub: { enabled: true } });
+ });
+ const failure = page.getByRole('alert');
+ await expect(failure).toContainText('WorkHub 暂时无法启动');
+ await expect(failure).toContainText('请检查当前 Runtime Host 的默认模型配置');
+ await assertAxHealth(cdp, 'workhub/startup-error');
+});
+
+test('a streaming answer exposes a healthy live conversation state', async ({ window: page }) => {
+ const cdp = await page.context().newCDPSession(page);
+ await enterMainFromSkipLink(page);
+ const composer = page.locator(COMPOSER_INPUT);
+ await tabTo(page, composer, 'streaming composer', 60);
+ await page.keyboard.insertText(FAKE_HOLD_OPEN_PROMPT);
+ const send = page.getByRole('button', { name: '发送' });
+ await tabTo(page, send, 'streaming Send button', 20);
+ await page.keyboard.press('Enter');
+
+ await expect(page.locator('.maka-bubble-streaming')).toContainText('Fake backend waiting');
+ await expect(page.getByRole('button', { name: '停止' })).toBeEnabled();
+ await assertAxHealth(cdp, 'conversation/streaming');
+
+ const stop = page.getByRole('button', { name: '停止' });
+ await tabTo(page, stop, 'streaming Stop button', 20);
+ await page.keyboard.press('Enter');
+ await expect(page.getByRole('button', { name: '重新生成' })).toHaveCount(1, {
+ timeout: 20_000,
+ });
+ await assertAxHealth(cdp, 'conversation/stopped');
+});
+
test('composer and workbar entry points expose named actionable controls', async ({
window: page,
}) => {
@@ -126,11 +248,15 @@ test('composer and workbar entry points expose named actionable controls', async
await expect(page.getByRole('region', { name: '新任务对话' })).toBeVisible();
await assertAxHealth(cdp, 'conversation/new-task');
+ await enterMainFromSkipLink(page);
const composer = page.locator(COMPOSER_INPUT);
const prompt = 'create a session for accessibility coverage';
- await composer.fill(prompt);
- await expect(page.getByRole('button', { name: '发送' })).toBeEnabled();
- await composer.press('Enter');
+ await tabTo(page, composer, 'new-task composer', 60);
+ await page.keyboard.insertText(prompt);
+ const send = page.getByRole('button', { name: '发送' });
+ await expect(send).toBeEnabled();
+ await tabTo(page, send, 'new-task Send button', 20);
+ await page.keyboard.press('Enter');
await expect(page.getByText(`Fake backend received: ${prompt}`)).toBeVisible({
timeout: 30_000,
});
@@ -159,6 +285,13 @@ test('composer and workbar entry points expose named actionable controls', async
const activeTab = page.getByRole('tab', { name: new RegExp(panel) });
await expect(activeTab).toBeVisible();
await expect(activeTab).toHaveAttribute('aria-selected', 'true');
+ if (panel === '终端') {
+ await expect(page.getByRole('region', { name: '任务终端' })).toBeVisible();
+ } else if (panel === '浏览器') {
+ await expect(page.getByRole('region', { name: '嵌入式浏览器' })).toBeVisible();
+ } else if (panel === '待办') {
+ await expect(page.getByRole('region', { name: '任务待办' })).toBeVisible();
+ }
await assertAxHealth(cdp, `workbar/${panel}`);
if (panel !== workbarPanels.at(-1)) {
await page.getByRole('button', { name: '打开工作栏标签' }).first().click();
diff --git a/apps/desktop/e2e/composer-directory-reference.spec.ts b/apps/desktop/e2e/composer-directory-reference.spec.ts
new file mode 100644
index 0000000000..d0636bd654
--- /dev/null
+++ b/apps/desktop/e2e/composer-directory-reference.spec.ts
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { COMPOSER_INPUT, expect, test } from './fixtures';
+
+test('a folder reference is removable, survives send/reload, and leaves project selection unchanged', async ({
+ directoryReferenceWindow: { page, folder },
+}, testInfo) => {
+ const composer = page.locator(COMPOSER_INPUT);
+ const project = page.locator('button.maka-workspace-picker');
+ // The composer can mount before TaskEntry loads the initial project selection.
+ // Compare the settled selection, not the generic label shown during loading.
+ const originalProject = '选择项目:无项目';
+ await expect(project).toHaveAttribute('aria-label', originalProject);
+ const pick = async (keyboard = false) => {
+ const trigger = page.locator('.maka-composer-plus-menu button').first();
+ await expect(trigger).toHaveAttribute('aria-expanded', 'false');
+ if (keyboard) {
+ // Exercise keyboard reopening as well. Astryx intentionally ignores pointer
+ // reopening within 50ms of dismiss; the native chooser mock returns instantly.
+ await trigger.press('ArrowDown');
+ } else {
+ await trigger.click();
+ }
+ await expect(trigger).toHaveAttribute('aria-expanded', 'true');
+ await page.getByRole('menuitem', { name: '引用文件夹', exact: true }).click();
+ await expect(trigger).toHaveAttribute('aria-expanded', 'false');
+ };
+
+ await pick();
+ const chip = page.locator('.maka-composer-context-drawer .maka-composer-attachment-token');
+ await expect(chip).toContainText('referenced-source');
+ await chip.getByRole('button').click();
+ await expect(chip).toHaveCount(0);
+ await pick(true);
+ await expect(chip).toContainText('referenced-source');
+ await expect(project).toHaveAttribute('aria-label', originalProject);
+ await composer.fill('请检查引用目录');
+ await page.screenshot({ path: testInfo.outputPath('directory-reference-staged.png') });
+ await composer.press('Enter');
+
+ const user = page.getByLabel('你发送的消息').first();
+ await expect(user).toContainText('请检查引用目录');
+ await expect(user).toContainText('referenced-source');
+ await expect(user).not.toContainText('README.md');
+ const transcript = page.getByRole('log');
+ await expect(transcript).not.toContainText('README.md');
+ await expect(transcript).not.toContainText('"status":"listed"');
+ await expect(transcript).not.toContainText('DO_NOT_READ_FILE_CONTENTS');
+ await expect(transcript).not.toContainText('deep.txt');
+ await expect(chip).toHaveCount(0);
+ await expect(page.getByRole('button', { name: '重新生成' })).toHaveCount(1, { timeout: 20_000 });
+
+ const sessions = await page.evaluate(() => window.maka.sessions.list());
+ expect(sessions).toHaveLength(1);
+ expect(sessions[0]!.cwd).not.toBe(folder);
+ await page.reload();
+ await expect(page.getByLabel('你发送的消息').first()).toContainText('referenced-source');
+ await expect(page.getByRole('log')).not.toContainText('README.md');
+ await page.screenshot({ path: testInfo.outputPath('directory-reference-sent.png') });
+});
diff --git a/apps/desktop/e2e/composer-plus-menu-stability.spec.ts b/apps/desktop/e2e/composer-plus-menu-stability.spec.ts
index 569df394bf..5c5275726b 100644
--- a/apps/desktop/e2e/composer-plus-menu-stability.spec.ts
+++ b/apps/desktop/e2e/composer-plus-menu-stability.spec.ts
@@ -69,6 +69,21 @@ async function rejectBridgeLatch(
);
}
+// The fake echo can paint before Runtime Host publishes terminal turn ownership.
+// Use the catalog's known-empty live set as the barrier before latching its next read.
+async function waitForSessionTurnToSettle(
+ page: import('@playwright/test').Page,
+): Promise {
+ await expect
+ .poll(async () =>
+ page.evaluate(async () => {
+ const sessions = await window.maka.sessions.list();
+ return sessions[0]?.runningTurnIds ?? null;
+ }),
+ )
+ .toEqual([]);
+}
+
/**
* Toggling Plan from the + menu must not move the menu.
*
@@ -266,6 +281,7 @@ test('two rapid Plan toggles land on the last requested state', async ({
await composer.fill('alpha-marker');
await composer.press('Enter');
await expect(page.getByText(/Fake backend received: alpha-marker/)).toBeVisible();
+ await waitForSessionTurnToSettle(page);
await page.getByRole('button', { name: '添加上下文' }).click();
const menu = page.getByRole('menu', { name: '添加上下文' });
@@ -300,6 +316,7 @@ test('a failed catalog refresh keeps the committed Plan state visible', async ({
await composer.fill('alpha-marker');
await composer.press('Enter');
await expect(page.getByText(/Fake backend received: alpha-marker/)).toBeVisible();
+ await waitForSessionTurnToSettle(page);
await page.getByRole('button', { name: '添加上下文' }).click();
const menu = page.getByRole('menu', { name: '添加上下文' });
@@ -322,6 +339,7 @@ test('latest Plan intent still reaches the Host after a catalog refresh fails',
await composer.fill('alpha-marker');
await composer.press('Enter');
await expect(page.getByText(/Fake backend received: alpha-marker/)).toBeVisible();
+ await waitForSessionTurnToSettle(page);
await page.getByRole('button', { name: '添加上下文' }).click();
const planRow = page.getByRole('menu', { name: '添加上下文' })
@@ -352,6 +370,7 @@ test('deleting the session while a toggle is pending settles clean', async ({
await composer.fill('alpha-marker');
await composer.press('Enter');
await expect(page.getByText(/Fake backend received: alpha-marker/)).toBeVisible();
+ await waitForSessionTurnToSettle(page);
await page.getByRole('button', { name: '添加上下文' }).click();
const menu = page.getByRole('menu', { name: '添加上下文' });
diff --git a/apps/desktop/e2e/composer-undo.spec.ts b/apps/desktop/e2e/composer-undo.spec.ts
new file mode 100644
index 0000000000..80c7946d8e
--- /dev/null
+++ b/apps/desktop/e2e/composer-undo.spec.ts
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type { Locator } from '@playwright/test';
+import { expect, test, COMPOSER_INPUT } from './fixtures';
+
+const TYPED = 'x';
+const PASTED = '中文 & "quoted"\r\nhttps://example.test/path?x=1&y=2\n第二行 <>&';
+const PASTED_AS_PLAIN_TEXT = PASTED.replace(/\r\n/g, '\n');
+const UNDO_SHORTCUT = process.platform === 'darwin' ? 'Meta+z' : 'Control+z';
+
+async function expectComposerText(
+ composer: Locator,
+ expected: string,
+): Promise {
+ await expect
+ .poll(() => composer.evaluate((element) => element.innerText.replace(/\r\n/g, '\n')))
+ .toBe(expected);
+}
+
+async function pastePlainText(composer: Locator, pasted: string): Promise {
+ await composer.evaluate((element, text) => {
+ const transfer = new DataTransfer();
+ transfer.setData('text/plain', text);
+ element.dispatchEvent(
+ new ClipboardEvent('paste', {
+ bubbles: true,
+ cancelable: true,
+ clipboardData: transfer,
+ }),
+ );
+ }, pasted);
+}
+
+test('plain-text paste is undone separately from prior typing', async ({ window: page }) => {
+ const composer = page.locator(COMPOSER_INPUT);
+
+ await composer.click();
+ await page.keyboard.type(TYPED);
+ await expectComposerText(composer, TYPED);
+
+ await pastePlainText(composer, PASTED);
+ await expectComposerText(composer, `${TYPED}${PASTED_AS_PLAIN_TEXT}`);
+
+ await page.keyboard.press(UNDO_SHORTCUT);
+ await expectComposerText(composer, TYPED);
+
+ await page.keyboard.press(UNDO_SHORTCUT);
+ await expectComposerText(composer, '');
+
+ await page.keyboard.press(UNDO_SHORTCUT);
+ await expectComposerText(composer, '');
+});
+
+test('pasted absolute path sends on the first Enter without opening the slash menu', async ({
+ window: page,
+}) => {
+ const composer = page.locator(COMPOSER_INPUT);
+ const pasted = '/Users/me/notes.txt';
+
+ await expect(page.locator('button.maka-workspace-picker')).toBeEnabled();
+ await composer.click();
+ await pastePlainText(composer, pasted);
+
+ await expect(composer).toHaveAttribute('aria-expanded', 'false');
+ await composer.press('Enter');
+ await expect(page.getByText(`Fake backend received: ${pasted}`)).toBeVisible();
+});
+
+test('pasted mention-looking text does not open the file menu', async ({ window: page }) => {
+ const composer = page.locator(COMPOSER_INPUT);
+
+ await expect(page.locator('button.maka-workspace-picker')).toBeEnabled();
+ await composer.click();
+ await pastePlainText(composer, 'review @name');
+
+ await expect(composer).toHaveAttribute('aria-expanded', 'false');
+ await expect(page.getByRole('listbox')).toHaveCount(0);
+});
+
+test('plain-text paste closes an existing trigger menu before the first Enter', async ({
+ window: page,
+}) => {
+ const composer = page.locator(COMPOSER_INPUT);
+ const pasted = 'Users/me/notes.txt';
+
+ await expect(page.locator('button.maka-workspace-picker')).toBeEnabled();
+ await composer.click();
+ await composer.pressSequentially('/');
+ await expect(composer).toHaveAttribute('aria-expanded', 'true');
+
+ await pastePlainText(composer, pasted);
+
+ await expect(composer).toHaveAttribute('aria-expanded', 'false');
+ await composer.press('Enter');
+ await expect(page.getByText(`Fake backend received: /${pasted}`)).toBeVisible();
+});
+
+test('plain-text paste synchronizes the controlled draft once', async ({ window: page }) => {
+ const composer = page.locator(COMPOSER_INPUT);
+
+ await expect(page.locator('button.maka-workspace-picker')).toBeEnabled();
+ await page.evaluate(() => {
+ const storageKey = 'maka-new-task-reload-intent-v1';
+ sessionStorage.setItem(storageKey, JSON.stringify({ draft: '' }));
+ const originalSetItem = Storage.prototype.setItem;
+ let writes = 0;
+ Storage.prototype.setItem = function setItem(key: string, value: string): void {
+ if (key === storageKey) writes += 1;
+ originalSetItem.call(this, key, value);
+ };
+ Object.defineProperty(globalThis, '__makaComposerDraftWrites', {
+ configurable: true,
+ get: () => writes,
+ });
+ });
+
+ await composer.click();
+ await pastePlainText(composer, 'one controlled change');
+
+ const writes = await page.evaluate(
+ () =>
+ (globalThis as typeof globalThis & { __makaComposerDraftWrites?: number })
+ .__makaComposerDraftWrites ?? 0,
+ );
+ expect(writes).toBe(1);
+});
diff --git a/apps/desktop/e2e/fixtures.ts b/apps/desktop/e2e/fixtures.ts
index f4c9f8ef5a..e3c7a46c2b 100644
--- a/apps/desktop/e2e/fixtures.ts
+++ b/apps/desktop/e2e/fixtures.ts
@@ -24,11 +24,9 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { promisify } from 'node:util';
-import {
- createProjectCatalog,
- createSessionStore,
- createSettingsStore,
-} from '@maka/storage';
+import { createProjectCatalog } from '@maka/storage/project-catalog';
+import { createSessionStore } from '@maka/storage/session-store';
+import { createSettingsStore } from '@maka/storage/settings-store';
import {
resolveStorageRoot,
tryAcquireInteractiveRootOwner,
@@ -56,6 +54,38 @@ export const PARENT_REMOVAL_CHILD_NAME = '应归档的子任务';
/** Directory basename, and so the Project name the workspace picker lists. */
export const NEW_TASK_PROJECT_NAME = 'new-task-project';
+/**
+ * Restore the navigation column through the titlebar action when a test needs
+ * controls that only exist in the expanded sidebar. The fixture starts with
+ * the sidebar collapsed, and the action follows the configured UI locale.
+ */
+export async function ensureSidebarExpanded(page: Page): Promise {
+ const expandSidebar = page.getByRole('button', {
+ name: /^(?:展开侧边栏|Expand sidebar)$/,
+ });
+ if (!(await expandSidebar.isVisible())) return;
+
+ await expandSidebar.click();
+ await expect(
+ page.getByRole('button', { name: /^(?:收起侧边栏|Collapse sidebar)$/ }),
+ ).toBeVisible();
+}
+
+/**
+ * Wait for the default Host's Coordination Session and the WorkHub projection
+ * to agree that the surface is ready. A mounted WorkHub main is not sufficient:
+ * it is also rendered while the Host reconnects and the projection reloads.
+ */
+export async function waitForWorkHubReady(page: Page, workCount: number): Promise {
+ await expect
+ .poll(async () => {
+ const snapshot = await page.evaluate(() => window.maka.runtimeHostProfiles.getSnapshot());
+ return snapshot.entries.find(({ isDefault }) => isDefault)?.readiness;
+ })
+ .toBe('ready');
+ await expect(page.getByText(`${workCount} 项工作`, { exact: true })).toBeVisible();
+}
+
/**
* Wait for Runtime's authoritative Skill projection, not merely for the
* composer DOM to mount. The renderer requests this projection after its first
@@ -150,6 +180,28 @@ async function seedE2eLocale(userDataDir: string, locale: 'zh' | 'en'): Promise<
});
}
+/** Rows for the rail-render contract: enough that a stray render is loud. */
+export const RAIL_RENDER_SESSION_COUNT = 12;
+
+async function seedRailRenderSessions(userDataDir: string): Promise {
+ const workspaceRoot = path.join(userDataDir, 'workspaces', 'default');
+ const store = createSessionStore(workspaceRoot);
+ try {
+ for (let index = 0; index < RAIL_RENDER_SESSION_COUNT; index += 1) {
+ await store.create({
+ cwd: path.join(userDataDir, 'project'),
+ llmConnectionSlug: 'e2e',
+ model: 'claude-sonnet-4-5-20250929',
+ permissionMode: 'ask',
+ name: `Rail row ${index}`,
+ labels: [],
+ });
+ }
+ } finally {
+ await store.close?.();
+ }
+}
+
async function seedParentRemovalSessions(userDataDir: string): Promise {
const workspaceRoot = path.join(userDataDir, 'workspaces', 'default');
const store = createSessionStore(workspaceRoot);
@@ -339,6 +391,7 @@ async function withE2eWindow(
invocableSkills,
gitReviewExtraFiles,
parentRemovalSessions,
+ railRenderSessions,
newTaskProject,
}: {
seed: boolean;
@@ -354,9 +407,10 @@ async function withE2eWindow(
invocableSkills?: boolean;
gitReviewExtraFiles?: number;
parentRemovalSessions?: boolean;
+ railRenderSessions?: boolean;
newTaskProject?: boolean;
},
- use: (page: Page, context: { userDataDir: string }) => Promise,
+ use: (page: Page, context: { userDataDir: string; app: ElectronApplication }) => Promise,
): Promise {
const userDataDir = await mkdtemp(path.join(tmpdir(), 'maka-e2e-'));
// Lives inside the throwaway userData dir so the existing teardown removes
@@ -369,6 +423,7 @@ async function withE2eWindow(
try {
if (seed) await seedE2eConnection(userDataDir);
if (parentRemovalSessions) await seedParentRemovalSessions(userDataDir);
+ if (railRenderSessions) await seedRailRenderSessions(userDataDir);
if (invocableSkills) await seedE2eInvocableSkills(userDataDir);
if (gitReviewExtraFiles !== undefined) {
await seedE2eGitReviewProject(userDataDir, gitReviewExtraFiles);
@@ -422,7 +477,7 @@ async function withE2eWindow(
const rendererDetail = rendererLogs.length > 0 ? `\nRenderer console:\n${rendererLogs.join('\n')}` : '';
throw new Error(`${detail}${mainDetail}${rendererDetail}`, { cause: error });
}
- await use(page, { userDataDir });
+ await use(page, { userDataDir, app });
} finally {
try {
if (app) await closeElectronApplication(app, 5_000);
@@ -440,11 +495,32 @@ export const test = base.extend<{
linkColorWindow: Page;
projectSidebarWindow: Page;
parentRemovalWindow: Page;
+ railRenderWindow: Page;
promptRailWindow: Page;
+ partialHistoryWindow: Page;
promptRailMotionWindow: Page;
requestHeaderRowWindow: Page;
newTaskTargetWindow: Page;
+ directoryReferenceWindow: { page: Page; folder: string };
+ accessibilityNarrativeWindow: Page;
}>({
+ directoryReferenceWindow: async ({}, use) => {
+ await withE2eWindow(
+ { seed: true, readinessSelector: COMPOSER_INPUT, locale: 'zh', showWindow: true },
+ async (page, { userDataDir, app }) => {
+ const folder = path.join(userDataDir, 'referenced-source');
+ await mkdir(path.join(folder, 'nested'), { recursive: true });
+ await writeFile(path.join(folder, 'README.md'), 'DO_NOT_READ_FILE_CONTENTS');
+ await writeFile(path.join(folder, 'nested', 'deep.txt'), 'DO_NOT_DESCEND');
+ // Replace only the OS chooser. IPC, Host admission, message delivery,
+ // event persistence and rendering still run through the real stack.
+ await app.evaluate(({ dialog }, selectedPath) => {
+ dialog.showOpenDialog = async () => ({ canceled: false, filePaths: [selectedPath] });
+ }, folder);
+ await use({ page, folder });
+ },
+ );
+ },
// Seeded: a pre-staged connection clears onboarding so the composer is ready.
window: async ({}, use) => {
await withE2eWindow({ seed: true, readinessSelector: COMPOSER_INPUT, locale: 'zh' }, use);
@@ -522,6 +598,17 @@ export const test = base.extend<{
use,
);
},
+ railRenderWindow: async ({}, use) => {
+ await withE2eWindow(
+ {
+ seed: true,
+ readinessSelector: COMPOSER_INPUT,
+ locale: 'zh',
+ railRenderSessions: true,
+ },
+ use,
+ );
+ },
// A multi-prompt transcript for the prompt anchor rail. Shown, because every
// assertion in prompt-rail.spec.ts is geometry the compositor has to settle.
promptRailWindow: async ({}, use) => {
@@ -536,6 +623,17 @@ export const test = base.extend<{
showWindow: true,
}, use);
},
+ // A transcript larger than the bounded Desktop range. Clicking an unloaded
+ // prompt exercises the real load-around path and its partial-history UI.
+ partialHistoryWindow: async ({}, use) => {
+ await withE2eWindow({
+ seed: false,
+ readinessSelector: '[data-turn-id]',
+ e2eFixtureScenario: 'chat-partial-history',
+ locale: 'zh',
+ showWindow: true,
+ }, use);
+ },
// The same transcript, scrolling the way the shipped app scrolls. Separate
// from `promptRailWindow` because it is only the jump that needs a scroll
// still in flight, and paying for one everywhere costs several seconds per
@@ -562,6 +660,18 @@ export const test = base.extend<{
showWindow: true,
}, use);
},
+ // A data-backed conversation with settled tool evidence and a populated
+ // task ledger. Shown because the accessibility journey follows real native
+ // focus order through the transcript into the composer controls.
+ accessibilityNarrativeWindow: async ({}, use) => {
+ await withE2eWindow({
+ seed: false,
+ readinessSelector: '[data-turn-id]',
+ e2eFixtureScenario: 'turn-narrative',
+ locale: 'zh',
+ showWindow: true,
+ }, use);
+ },
});
export { expect };
diff --git a/apps/desktop/e2e/goal-dialog-budget.spec.ts b/apps/desktop/e2e/goal-dialog-budget.spec.ts
index 28f7d1efc1..14b56668e0 100644
--- a/apps/desktop/e2e/goal-dialog-budget.spec.ts
+++ b/apps/desktop/e2e/goal-dialog-budget.spec.ts
@@ -82,4 +82,20 @@ test('an unsendable budget blocks Start instead of arming a different one', asyn
};
})
.toEqual({ condition: '所有测试通过', maxIterations: 25, tokenBudget: 5000 });
+
+ // The Host read above proves persistence; this proves the same broadcast now
+ // reaches the provider-owned chat projection without AppShell reading it.
+ const goalContext = page
+ .getByRole('region', { name: '任务上下文' })
+ .filter({ visible: true });
+ await expect(
+ goalContext
+ .getByText(/目标 0 \/ 25 · .* · 0 \/ 5k/)
+ .filter({ visible: true }),
+ ).toBeVisible();
+ await expect(
+ goalContext
+ .getByRole('button', { name: '暂停自主执行目标(已进行 0/25 轮)' })
+ .filter({ visible: true }),
+ ).toBeVisible();
});
diff --git a/apps/desktop/e2e/module-hub.spec.ts b/apps/desktop/e2e/module-hub.spec.ts
new file mode 100644
index 0000000000..91609cfcfd
--- /dev/null
+++ b/apps/desktop/e2e/module-hub.spec.ts
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { expect, test } from './fixtures';
+
+test('Module Hub switches all four leaves and opens scheduled creation once', async ({
+ window: page,
+}) => {
+ const expand = page.getByRole('button', { name: '展开侧边栏' });
+ if (await expand.isVisible()) await expand.click();
+ const sidebar = page.getByRole('navigation', { name: '任务列表' });
+
+ await sidebar.getByRole('button', { name: '扩展', exact: true }).click();
+ await expect(page.locator('[data-module="skills"]')).toBeVisible();
+ const extensions = page.getByRole('navigation', { name: /扩展内容/ });
+ await extensions.getByRole('button', { name: 'MCP', exact: true }).click();
+ await expect(
+ extensions.getByRole('button', { name: 'MCP', exact: true }),
+ ).toHaveAttribute('aria-current', 'true');
+
+ await sidebar.getByRole('button', { name: /定时任务/ }).click();
+ await expect(page.locator('[data-module="scheduled-tasks"]')).toBeVisible();
+ const automations = page.getByRole('navigation', { name: /定时任务内容/ });
+ await automations.getByRole('button', { name: '每日回顾', exact: true }).click();
+ await expect(page.locator('[data-module="daily-review"]')).toBeVisible();
+
+ await page.keyboard.press(process.platform === 'darwin' ? 'Meta+k' : 'Control+k');
+ const palette = page.getByRole('dialog', { name: '命令面板' });
+ await expect(palette).toBeVisible();
+ await palette.getByRole('option', { name: /新建定时任务/ }).click();
+
+ const createDialog = page.getByRole('dialog', { name: '新建定时任务' });
+ await expect(createDialog).toBeVisible();
+ await expect(createDialog).toHaveCount(1);
+ await expect(page.locator('[data-module="scheduled-tasks"]')).toBeVisible();
+});
diff --git a/apps/desktop/e2e/native-transcript-perf.spec.ts b/apps/desktop/e2e/native-transcript-perf.spec.ts
new file mode 100644
index 0000000000..4c7eafb09b
--- /dev/null
+++ b/apps/desktop/e2e/native-transcript-perf.spec.ts
@@ -0,0 +1,441 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type { CDPSession, Page } from '@playwright/test';
+import { PROMPT_RAIL_PROMPT_COUNT } from '../src/main/e2e-fixture/seed-helpers';
+import * as transcriptContract from '../src/preload/transcript-contract';
+import { ensureSidebarExpanded, expect, test } from './fixtures';
+
+const PERF_ENABLED = process.env.MAKA_TRANSCRIPT_PERF === '1';
+const STRESS_ENABLED = process.env.MAKA_TRANSCRIPT_STRESS === '1';
+const SCROLLER = '[data-chat-scroll-container="true"]';
+
+interface BrowserCounters {
+ heapBytes: number;
+ nodes: number;
+ documents: number;
+ jsEventListeners: number;
+}
+
+interface FrameSample {
+ intervals: number[];
+ loafDurations: number[];
+ loafSupported: boolean;
+}
+
+interface StressSample extends BrowserCounters {
+ sweep: number;
+ iteration: number;
+ firstTurnId: string | null;
+ lastTurnId: string | null;
+ mountedTurns: number;
+}
+
+interface StressSweep {
+ sweep: number;
+ successfulPages: number;
+ samples: StressSample[];
+}
+
+interface HeapGrowth {
+ endpointRatio: number;
+ slopeBytesPerPage: number;
+ projectedRatio: number;
+}
+
+/** The predeclared secondary heap/DOM release gate permits at most 10% growth. */
+const SECONDARY_RESOURCE_GROWTH_RATIO = 0.1;
+
+function positiveHeapGrowth(samples: readonly StressSample[]): HeapGrowth {
+ const first = samples[0]!;
+ const last = samples.at(-1)!;
+ const meanIteration = samples.reduce((sum, sample) => sum + sample.iteration, 0)
+ / samples.length;
+ const meanHeap = samples.reduce((sum, sample) => sum + sample.heapBytes, 0)
+ / samples.length;
+ const slopeNumerator = samples.reduce(
+ (sum, sample) => sum + (sample.iteration - meanIteration) * (sample.heapBytes - meanHeap),
+ 0,
+ );
+ const slopeDenominator = samples.reduce(
+ (sum, sample) => sum + (sample.iteration - meanIteration) ** 2,
+ 0,
+ );
+ const slopeBytesPerPage = slopeDenominator === 0 ? 0 : slopeNumerator / slopeDenominator;
+ const iterationSpan = last.iteration - first.iteration;
+ return {
+ endpointRatio: Math.max(0, last.heapBytes - first.heapBytes) / first.heapBytes,
+ slopeBytesPerPage,
+ projectedRatio: Math.max(0, slopeBytesPerPage * iterationSpan) / first.heapBytes,
+ };
+}
+
+function percentile(values: readonly number[], probability: number): number {
+ if (values.length === 0) return 0;
+ const ordered = [...values].sort((left, right) => left - right);
+ return ordered[Math.min(ordered.length - 1, Math.ceil(probability * ordered.length) - 1)]!;
+}
+
+async function collectGarbage(cdp: CDPSession): Promise {
+ await cdp.send('HeapProfiler.enable');
+ await cdp.send('HeapProfiler.collectGarbage');
+}
+
+async function browserCounters(cdp: CDPSession): Promise {
+ const [heap, dom] = await Promise.all([
+ cdp.send('Runtime.getHeapUsage'),
+ cdp.send('Memory.getDOMCounters'),
+ ]);
+ return {
+ heapBytes: heap.usedSize,
+ nodes: dom.nodes,
+ documents: dom.documents,
+ jsEventListeners: dom.jsEventListeners,
+ };
+}
+
+async function performanceMetrics(cdp: CDPSession): Promise