Skip to content

Legacy fork v2 dsh plugin - #182

Closed
giveUphope wants to merge 320 commits into
google:mainfrom
giveUphope:legacy-fork-v2-dsh-plugin
Closed

giveUphope wants to merge 320 commits into
google:mainfrom
giveUphope:legacy-fork-v2-dsh-plugin

Conversation

@giveUphope

Copy link
Copy Markdown

No description provided.

giveUphope and others added 30 commits October 9, 2026 01:25
…icker

Opening the notes popover momentarily overflowed the document by ~1px
while Arco positioned the popup, so Windows classic scrollbars were
inserted and removed within a frame — body went 1067x926 -> 1052x911 ->
1067x926 and the whole 100vh layout visibly jumped in both dimensions.
Reserve the vertical scrollbar gutter on <html> and forbid horizontal
body overflow so no transient overflow can ever insert a scrollbar.
Five rapid submissions into a dead device each spawned, failed with the
same 'Device not available' error and burned the whole queue. A task
whose session ends 'failed' without a single recorded step is now an
environment fault: the device's queue stops dispatching, the remaining
messages stay pending (retained and visible), and the error surfaces
only at the breakpoint round. The hold lifts automatically once a real
device enumeration lists the serial again, or when the user submits a
new task for that device or stops everything; task-level failures
(steps exist) keep the queue running.
DataEngine auto-connects to the desktop IPC bridge through the shared
port file under %TEMP%, so running the data_engine suite while the
console server was up broadcast every test session as 'running' — the
frontend's runId fallback materialized them and the tracking map kept
them forever. data_engine tests now resolve no IPC port (IPC-specific
tests re-patch the seams), and mergeSessions expires tracking entries
that no status payload mentions and no DB row backs after a 60s grace,
so any future phantom ages out instead of sticking around.
Review findings landed: the data_engine IPC hermeticity guard hoists to
the root tests/conftest.py so the four DataEngine-constructing files
outside that package cannot stream sessions into a live console either;
has_steps gains a real-SQLite schema test (a wrong column name would
have silently disabled the queue hold); the device hold adds a
per-device dispatch isolation test, queue_held/queue_resumed payload
assertions, probe-throttle/exception coverage and teardown symmetry.
Frontend locks the replay-center viewer (dual/single layout, nav bounds,
lightbox keyboard, step-row sync) and the post-shot backfill with
same-image skip, wires the draft-conversation flow at panel level, and
asserts the round-node error tooltip. AGENTS.md records the test-sync
review habit.
The circuit breaker skipped tasks without an explicit serial (the UI's
default), so a burst of auto-device submissions burned the whole queue
with identical device-unavailable failures. Holds now key the endpoint
default queue for auto tasks — dispatch skip, resume probe (any usable
device) and submission lift all follow the same key. Failures are also
classified: environment (zero-step, or mid-run errors matching
device/LLM/network markers) holds the queue as unacceptable, while
task-level failures (steps executed, no environment markers) stay
acceptable and keep the queue running.
Mainstream agent pattern (Claude Code queues by default with visible,
removable chips; Devin/Manus-style session pause/resume with a reason
banner) applied to the ARTEMIS console:

- POST /api/queue/pause and /api/queue/resume: manual pause gates the
  dispatcher (pending retained, running task unaffected); resume lifts
  the pause AND the environment circuit-breaker holds. /api/status now
  exposes queue_paused and queue_holds.
- CommandDock grows a queue strip: pending messages of the viewed thread
  render as removable chips, the held/paused states show a reason banner
  with a resume action, and a pause action appears while tasks run.
- Store captures queue_paused/queue_holds, exposes threadPendingRounds
  and pauseQueue/resumeQueue/removeQueuedRound.
Adding the queue pause/resume endpoints inserted them between
@api/queue/status's decorator and get_status, so GET /api/status
actually invoked pause_queue: every frontend status poll paused the
queue, the real status payload never reached the client, and typed
messages showed up as separate pending conversations instead of dock
chips. The decorators are now attached correctly and a router
registration guard test locks the method/path/endpoint mapping.
…sages vertically

Workers dying before DataEngine.start_session left no DB row, so the round
silently vanished from the timeline: the queue then held sibling messages
that no longer matched the visible rounds, and the failure surfaced only in
the dock queue strip instead of the message area. The queue service now
inserts a minimal failed row (goal/thread/submitted_at plus the worker's
stdout error tail) for that window, and the dock renders queued messages as
a vertical FIFO list that grows with content.
…drop the state blurb

The held/paused queue states are global, so a newly created conversation
still showed the previous thread's residual queue with its resume button --
clicking it there would resume another thread's queue. Queue controls now
render only when the viewed thread has messages in the queue or a live
task, and the held/paused explanation text is gone (the reason moved to
the resume button's hover title; the failure itself lives in the timeline).
…ched

A pending message rendered both as a queue chip in the dock and as a
queued round in the timeline, which read like two copies of the same
message. The timeline now filters pending rounds out -- they live only in
the dock queue list -- while round numbering still counts them, so a round
enters the timeline at its original position once it actually runs.
The next message's submission target is the thread being viewed, so any
involuntary view switch silently redirected it into another conversation's
queue. Beyond the status poll, the SSE handlers (session_started and
startup_progress) also yanked the view to any session that started while
the user was unpinned -- faster than the poll, so a task starting between
two messages was enough to misroute the next one.

Auto-follow is now gated by a shared mayAutoFollowSession admission (same
thread, or no thread being viewed) across the poll and both SSE sites, and
the fetchSessions fallback prefers the newest round of the current thread
and skips selection while that thread still has queued/running rounds.
…ion_ended

When the last active task ended, the SSE handler promoted the head of the
pending queue to the global running state. With the queue suspended by the
environment circuit-breaker nothing would dispatch, yet the conversation
owning that pending round showed a running badge (and the round leaked
into the timeline as an empty running round) until the next status poll
corrected it. The global state now derives from real active tasks only;
actual dispatch takes over via session_started / the status poll.
…s before DSH work

Five parallel audits (console-queue / runtime-persistence / device-media /
agent-stack / frontend-integration) enumerate the behaviors, invariants and
contracts that todo.md did not record and that would silently drift away
mid-migration. Key reconciliations, all verified against source:

- S2 exit-code mapping contradicted py reality (blocked/flash-failed tasks
  exit 0; cancel exits 130) - terminal status is DataEngine-DB-authoritative
- cancel_requests demoted from 'delete' to phase-1 keep: on Windows it is
  the only graceful-cancel channel (kill cannot interrupt the worker)
- S5 env contract was a subset; missing ARTEMIS_TASK_WORKER=1 makes the
  worker self-submit back to the legacy daemon
- queue pause/resume reclassified from agent.cancel to a dispatcher gate
- graph has no langgraph checkpointer: no resume semantics to migrate
- line-count drift corrected (task_queue_service 2027, device_lock 1028,
  media_service 623, diagnostics ~4400); system.py split out of replay row

Roadmap gains S9 (mid-task instruction injection) and gaps G8-G29 with an
authoritative index in docs/migration/feature-inventory.md (hard-block
contradictions, unified gap table, drift-risk top 10, open decisions).
Phase 1 introduces DSH alongside the existing stack (dual-track behind a
grayscale switch): no module removals or wholesale rewrites; only minimal,
explicitly-logged adaptations (D8 relaxed from 'zero new py code' to
'minimal co-existence changes').

Phase 2, gated on confirmed migration, removes modules DSH natively covers
and starts plugin-ization of the must-self-build modules; DSH capability
gaps must be closed through official extension seams (plugin API / toolview
/ webhook / Inbox / jobs / connection.fetch) for feature parity - modifying
the DSH runtime itself is off-limits.

Phase 3 produces plugin optimization directions on top of DSH as the core
(mode-a incremental relocation of ARTEMIS execution logic, structured
toolview rendering, LLM seam reliability alignment, conformance expansion).

Checklist P0-P4 annotated with phase ownership; G1 split accordingly; a
phase-1 adaptation register is added as a gating checklist item.
…umptions

Extracted the locally installed DSH desktop runtime (app.asar) and ran four
parallel source-level audits over 48 plan assumptions (jobs, session+Inbox,
plugin seams, SDK+CLI). Core mechanisms all held: JobSpec.owner session
fencing, durable two-list Inbox, session log v4 JSONL+zstd with generation
migration, external SessionId with idempotent adopt, enforced peer
compatibility, dsh.bundle.patch install flow, allowed-once fail-closed
approvals, Windows ACL sandbox without Docker.

Corrections written back to the roadmap (all evidenced in
docs/migration/dsh-verification.md):
- bash job kind is POSIX-only on Windows: phase 1 goes straight to a custom
  producer plugin (kind artemis-worker) copying the tool-bash/tool-pwsh
  samples; DSH itself never maps non-zero exit to failed - the producer's
  done hook carries the DB-authoritative status rule (G8)
- resume restores Inbox pending but does not auto-kick: startup
  reconciliation adds an explicit wake-up (D5)
- workspace/session-stop drops the pending queue (cancel without
  keepInbox); per-task stop with queue preservation uses
  agent.cancel(cause, {keepInbox}) + updateQueue
- DSH has no outbound webhook: notification adapter is plugin-side over
  ctx.jobs.events settled (G21)
- media hosting seam is ctx.webServer.register(), not ctx.connection.fetch
- toolview is a client-side slot plugin, not a host-side one (G2)
- hot reload is config-layer only (dsh-hmr); patchReload field is inert in
  rc.2 - code changes still need a profile restart (P0 dev flow)
- G6 closed: Windows ACL sandbox needs no Docker; G20 closed: catch-up is
  native (readSessionState/page/projections + live events)
- env scrub: credential-like keys (KEY/PASSWORD/SECRET/TOKEN) must be
  passed explicitly via request.env (G9/S5)
- phase 2 gains an option: dsh-mcp-client can connect the existing ARTEMIS
  mcp_server as a zero-code tool source, to be evaluated against the
  autogamer-device rewrite
Round two asks the opposite question of round one: for everything the plan
still marks as self-built, does DSH already ship something better? Findings
(evidenced in dsh-verification.md §5) and roadmap updates:

- QueueDock is native: queued-message chips with edit/remove/steer on the
  conversation input dock - the ARTEMIS dock queue equivalent already
  exists; G19 thread-aggregation scope shrinks to ~30% (round timeline
  view, goal-round visual distinction, round badges)
- credentials management is native (resolve/describe/set/unset, masked
  describe, .credentials.yaml layering, hot rotation): phase-2 keeps only
  a connectivity test
- tool timeouts are a native contract (ToolDefinition.timeoutMs fused into
  exec.signal, structured TOOL_TIMEOUT): custom tools must honor the
  signal and kill their processes - written into the autogamer-device spec
- plugin state persistence: ctx.storageDomain (zod + durable writes +
  changed events), dsh-atomic-write withFileLock - no hand-rolled JSON
- no library-level semaphore/mutex exists: the S3 gate starts from the
  weak-map promise-chain sample (tool-bash-persistent) plus dsh-deque
- run_adb_command persistent terminal + stdin maps to ctx.terminals
  (startSend/read/signal), not the persistent shell tools which explicitly
  do not support interactive stdin
- phase-2 gate/approval interception seam: tools/pre-execute waterfall
  (allow/deny/ask/cancel) - no need to wrap tools; hook-protocol is a
  Claude Code/Codex compatibility layer and must not be used
- media display: prefer the present tool (deliverables cards, replayable);
  no native inline video player in chat (client UI extension needed -
  product decision)
- also recorded: session-stats projections, file upload pipeline,
  fs-observation-policy is a write guard (not a watcher), http-proxy is
  unrelated to model routing
Self-review of the落地 plan after three verification rounds surfaced the
one blind spot none of them covered: the plan maps user messages to Inbox
followups but never says who turns a message into a job - as written, every
round would run a plain LLM conversation turn. Verified against source and
added S10 with three candidate routes:

- B1 (recommended): register a custom Agent via the dsh-agent factory slot
  (the same first-class seam ReactLoopAgent uses) - claim next-turn, no
  model call, gate -> spawn job -> await settle -> write results back as
  tool/call+result pairs; zero LLM cost
- B2 fallback: stock loop + model-less preset + agent/pre-step rejection
  (verified: rejected decision opens no step, so no model call)
- A last resort: minimal-instructions preset + spawn tool (one cheap LLM
  call per round; no tool_choice, so prompt-dependent)

Plus cross-cutting verified facts: creating an agent does not require a
model (enforced only at dispatch; agent/request can supply it),
ctx.jobs.start() is armed only when dsh-tool-jobs is mounted
(completionDelivery: quiet available), and inbox inserted/claimed/discarded
events are host-subscribable.

Scope trims to avoid wasted phase-1 work: the autogamer-device skeleton
moves out of P0 to P2, and the G21 webhook notification adapter moves to
P4 (during dual-run the legacy queue service still owns notifications).
P1 gains tool-jobs arming, maxConcurrentJobsPerOwner (default 10) and a
wake-up-with-pending reconciliation step; P0 gains a host/client plugin
packaging spike (toolview/round-timeline are client-side).
User concern: the custom-driver injection does not fit DSH well; survey how
mainstream agent harnesses invoke tasks before re-conceiving. Five samples
(Claude Code -p, Codex exec, Gemini -p, ACP, OpenClaw gateway) agree on one
rule: the harness invocation unit IS the task, queueing/scheduling lives
outside the harness, and the turn is always a model call - nobody replaces
the turn model. DSH's 'dsh --profile headless' is structurally identical to
'claude -p' / 'codex exec' and already ships.

S10 re-ranked accordingly:
- A (mainstream-aligned): headless-per-task - the queue plugin owns a
  durable queue (storageDomain) + device gate, spawns 'dsh --profile
  headless --session-id <conversation> <goal>' as the DSH job; running-task
  visibility equals the phase-1 py-worker black box; and it gives mode-a a
  shell-swap path (py worker -> headless agent) before logic extraction
- B (full UI): tool-mediated - native Inbox/QueueDock/timeline, one cheap
  local LLM call per round
- custom Agent factory driver demoted to last resort (off-mainstream,
  internal seam, rc.2 upgrade fragility)

P0 S10 spike becomes an A-vs-B comparison.
D10: the project is consumed AS a DSH plugin - there are no external HTTP
callers, so no HTTP-to-SDK bridge gets built; artemis-client is deleted
outright with no migration list; webhook notification adapter drops to
build-on-demand; idempotency/debounce semantics downgrade from external
promises to internal plugin-queue behavior; the mcp-client direct-connect
option becomes transition-only (native DSH plugin tools preferred).

User decisions recorded:
- S10: spike both routes (headless-per-task and tool-mediated) in P0 and
  decide on data
- G25: replicate the accessibility helper in phase 2 (keep Mobly/Appium
  co-existence differentiation)
- S6: run_adb_command as whole-tool ask (allowed-once) to start; read-only
  allowlist deferred to phase 2

Open-decision list in feature-inventory updated: 8 items resolved or given
engineering defaults, one (queue signal source) deferred to the S10 spike
outcome.
… in DSH

User ruling: queueing and scheduling are exactly what DSH already provides;
the plugin should only supply content for DSH to invoke, so it always rides
DSH version evolution. The headless-per-task route (previous S10-A) needed a
plugin-side durable queue, contradicting our own D2 decision (Inbox-native
queues) - rejected. The mainstream 'queue outside the harness' pattern only
applies to CLI tools without a resident UI and durable inbox; DSH's Inbox +
QueueDock + resident host is precisely its advantage over claude -p style
tools.

S10 final architecture (tool-mediated):
- DSH Inbox = session queue (native QueueDock UI), agent loop = scheduler
- autogamer plugin = pure content: run_device_task tool (device gate ->
  ctx.jobs.start artemis-worker job -> await settle -> result back),
  device breaker/admission probing as tool-internal resource management
  (the only self-built left - DSH has no device-domain primitives), and an
  autogamer preset (minimal instructions, ctx.tools.restrict whitelist,
  spawn tool approval=never)
- completion semantics (await single-turn vs return+notice) decided by the
  P0 spike; small startup patch kicks sessions with pending inbox (the one
  verified D5 gap - domain glue, not orchestration)

P0 S10 spike now validates four points of the chosen route: model dispatch
stability under a local cheap model + guard, long-blocking await tool
cancel/progress behavior, pending kick, and multi-message Inbox queueing
visibility in QueueDock. G18 signal-source question closes with this
ruling.
… rule

Re-audited the self-build list through the 'plugin provides content only'
lens. Removed:

- autogamer-queue as a plugin: the queue IS the DSH Inbox (D2); the device
  gate, circuit breaker, admission probing and global concurrency cap fold
  into the run_device_task tool inside autogamer-device
- autogamer-media as a plugin: present/deliverables natively carry
  recordings; a webServer route is build-on-demand only if native serving
  proves insufficient
- G13 idempotency/debounce: submissions are DSH Inbox user messages - no
  double-click or retry surface; only an optional short same-goal debounce
  inside the tool remains
- G19 custom round-timeline view: rounds are native turns with native
  timeline/queue presentation; P3 only evaluates a goal-round badge

Orphan reconciliation and pending kick are repositioned as py-era glue that
retires with the py worker. Phase 1 now ships exactly one plugin package:
autogamer-device (run_device_task tool + device resource management +
preset); media becomes present integration and presets become skills in
phase 2.
…line

Per user direction, re-derived the migration plan from the unmodified
upstream project (this repo is a fork of google/artemis, 92 commits ahead
with the smart stack essentially untouched).

Findings (upstream-rethink.md):
- three canonical invocation surfaces: MCP (README headline - 'let AI
  assistants use real phones', structurally identical to the DSH
  content-plugin positioning), playground cloud (one full container per
  session with a dedicated Cuttlefish AVD - no shared queue exists
  upstream), and the local console (the shared-device shape our fork
  hardened)
- upstream container env contract is 4 variables; our fork's 20+ env
  (ticket/ingress/submitted_at/lock-scope) are shared-queue complexity
  that upstream never had
- fork drift: +1292 lines in admin_console (queue enhancements), smart
  stack essentially unchanged - existing inventories 03/04 remain valid
  as upstream-canonical contracts; inventory 01 largely fork-scenario
  legacy

Re-baselined plan (R2-prime): direct-to-plugin with MCP reuse of the py
action layer, flash = agent loop + skills, pro verification implemented in
phase 1 via DSH subagents with faithful invocation semantics (validator
incident hand-back, checker read-only zero-side-effect, verify-loop vs
assert accounting), recording deferred. One open fork: device binding
model (shared physical device with full gate vs per-session emulator with
thin validation).
…gate

Phase 1 keeps the complete gate/circuit-breaker/admission semantics
(inventory 01 section 1.2) inside run_device_task, matching the current
single-machine shared-device reality. Per-session dedicated emulators
(the upstream playground model) are recorded as the phase-2 evolution
path, at which point the gate degrades to thin validation.

The migration plan is now fully re-baselined with no open architectural
forks: R2-prime direct-to-plugin, MCP reuse of the py action layer, flash
via agent loop + skills, pro via subagents with faithful invocation
semantics in phase 1, recording deferred, one plugin package
(autogamer-device).
User proposal: merge the flash/pro two layers into one, with a router that
decides when the short path suffices and when deeper exploration is needed.

Key insight from the upstream study: the validator was already a per-action
escalating router (XML match -> coordinate self-heal -> pixel VLM dual net);
the flash/pro difference was never two execution bodies, it was verification
density. So the merge makes density a code-enforced deterministic route
instead of a profile choice:

- one agent loop + one run_device_action tool (the router) + two read-only
  subagent providers (validator/checker) + goal tool as optional plan
  scaffolding
- router order per step, code-enforced (invocation-faithful): target
  confidence -> short path direct execution; risk class -> validator
  pre-check + approval ask; failure -> self-heal ladder -> threshold ->
  checker audit before retry; plan present -> milestone audits, else exit
  audit only
- off/final/checkpoints/strict survives as router strictness config (a
  run_device_task parameter), not as two execution bodies
- faithfulness: validator escalation ladder unchanged; checker three
  invariants preserved (read-only subagent, append-only decisions in the
  session log, release/verdict separation); run_outcome dual axis becomes
  the report tool's completed/blocked + tests.failed; budgets =
  goal maxGoalRounds (verified round-limit->blocked) + checker retry cap

Trade-offs recorded: high skill/prompt hygiene for the single agent, and
route misclassification (over/under-verification) tuned via route-decision
logging added to P1.
…h SOTA

Question: for the single-layer agent, does the industry have precedents of
using a plain agent while making it behave as expected through external
means? Answer: yes - and it is the convergence direction.

Findings (dsh-verification section 8):
- Anthropic's workflows-vs-agents guidance: five workflow patterns
  (routing, parallelization, evaluator-optimizer...) give predictability;
  agent loops give openness. S11's router is an action-level workflow.
- Mobile GUI agent research explicitly critiques reflection-style
  self-checking (a model reviewing its own actions) and moves to
  verifier-driven paradigms with logic-based action verification - exactly
  S11's code-enforced router + read-only validator/checker subagents.
- Action-effect verification (did the screen actually change) is the
  accepted failure-detection signal - adopted into the S11 router as the
  trigger for the self-heal ladder (py utils image_diff/image_hash
  already provide the signal).
- DSH natively ships the code-mode/PTC pattern (dsh-workflow +
  dsh-ptc-runtime, verified in the extracted source) - the ready-made
  mechanism for deterministic action bursts in phase 3.

S11 router step 3 now includes post-action effect confirmation before the
self-heal ladder.
The roadmap had grown into geological layers (seven audit rounds of
inline corrections, superseded routes, closed gaps stacked on top of each
other). Per the repo's living-doc convention, rewrite it as the final
four-layer plan:

1. Final shape: one plugin (autogamer-device) + skills; DSH host provides
   queue/scheduler/UI/approvals/credentials/timeouts/sandbox/present;
   py reused via the mcp action face; smart stack not migrated (S11
   replaces it); old console stays read-only.
2. Decisions D1-D14 in their current, amended form (route = direct-to-
   plugin with a quality spike as gate; shared device + full gate;
   single-layer router; adb whole-tool ask; helper replication; recording
   deferred; no external API surface).
3. Integration semantics S1-S11 updated to the R2' final form - notably
   S2 (task = turn-wide action sequence via tools, the job mapping is
   R1 legacy), S4 (breaker classifies tool-layer errors, no DB probing),
   S5 (py action server contract, no bare dsh-mcp-client), S11 (router
   with post-action effect verification).
4. Gap list reduced to the 15 still-active items with adjusted scopes;
   the 15 closed ones are listed in one line with pointers to git
   history. Checklist P0-P4 rewritten as R2' milestones (P0 carries the
   dual spike: S10 four-point validation + MCP quality comparison that
   gates direct-vs-fallback).
…plugin

First working implementation of the R2' plan (docs/todo.md): a DSH content
plugin that provides device execution while the host supplies queueing
(Inbox/QueueDock), scheduling (agent loop), UI, approvals and memory.

Contents (per S3/S4/S5/S11):
- DeviceGate: per-key FIFO wait-chain with tracked pending; abort listener
  attached synchronously so queued waiters bail out before their turn;
  admission/release state kept in separate flags (the conflation of the two
  deadlocked the chain - caught by the FIFO test)
- DeviceBreaker: closed/open/half-open on environment-level failures with
  single-probe half-open; task failures never count
- classify: tool-layer error classes from ActionResult codes + transport
  errors (replaces the py-era DB-probing rule)
- ActionClient: MCP stdio wrapper around the upstream py action server
  (artemis mcp --server adb); deliberately not dsh-mcp-client, whose
  registered tools would bypass the gate; timeout tears the session down
  (mcp-1.29 lesson)
- run_device_action: the single router tool (S11 skeleton) - breaker
  admission, gate acquire with exec.signal abort, forwarding, effect-OK
  classification, route-decision logging for threshold tuning
- report_task_status: run_outcome dual-axis end signal (completed/blocked
  + tests_failed)
- autogamer preset declaration + flash execution skill draft
- dsh-types.ts mirrors the verified DSH contract surface (no shipped
  types); re-check on every DSH upgrade

13 vitest cases green (gate FIFO/abort/counts, breaker state machine,
classification). P0 spikes still open and tracked in the README:
install-flow + preset child-entry shape, model dispatch stability, effect
verification signal, MCP quality comparison (mock driver first).
… real py contracts

P0 spike results, all against the live 0.2.0-rc.2 runtime:

Install flow (passed, with three hands-on corrections now encoded):
- package.json must declare dsh.bundle.patch - without it the plugin
  installs as a plain dependency and never enters dsh.profile.bundles
- bundle patches are insert lists ('- insert: [rows]'); id-targeted rows
  warn unmatched for brand-new entries
- dsh plugin add takes Windows-style paths (pnpm is a native process) and
  a remove+add cycle is needed to re-evaluate the manifest
- verified: link: install, autogamer-device in dsh.profile.bundles,
  --dump-config shows the composed layer, exit 0

MCP direct-connect (offline leg passed):
- ActionClient <-> 'uv run artemis mcp --type adb' round-trip: exact
  13-tool surface matched, result normalization verified, adb-down
  failures classified as environment through the string contract
- real py contracts wired (read from artemis/mcp/adb_server.py, replacing
  inventory-derived guesses): tool names tap/long_press_on/swipe/back/
  launch_app/stop_app/open_link/focus_and_input_text/focus_and_clear_text/
  erase_one_char/press_key/take_screenshot/get_ui_hierarchy; legacy PIXEL
  coordinate contract with 0-1000 conversion at the tool boundary
  (src/coord.ts, dimensions probed from screenshot PNG IHDR / JPEG SOF);
  plain-string results ('Success'/'Failed'/'Error: ...'/'Error executing
  tool ...') normalized in normalizeOutcome; KEYCODE_* mapping; CLI flag
  is --type not --server
- get_ui_hierarchy text-target resolution (findNodeCenterBySubstring/
  ByIndex) so the flash skill can target elements without coordinates

13 vitest cases green; spike script at scripts/spike-mcp.mjs (mock-driver
offline mode). Remaining P0 spikes: live-profile S10 four-point validation
and the real-device quality comparison.
…tracts; S10 findings

P0 spike session results, all against the live 0.2.0-rc.2 runtime and the
local LM Studio endpoint:

Install flow (passed, three hands-on corrections now encoded in README):
- package.json must declare dsh.bundle.patch or the plugin installs as a
  plain dependency and never enters dsh.profile.bundles
- bundle patches are '- insert:' lists; id-targeted rows warn unmatched
- dsh plugin add needs Windows-style paths; remove+add re-evaluates the
  manifest

MCP direct-connect (offline leg passed):
- ActionClient <-> 'uv run artemis mcp --type adb': exact 13-tool surface,
  string-result normalization ('Success'/'Failed'/'Error:...'/'Error
  executing tool...'), adb-down classified as environment
- real py contracts wired from artemis/mcp/adb_server.py: legacy PIXEL
  coordinates (0-1000 conversion at the tool boundary via probe-screenshot
  dimensions), real tool names (tap/long_press_on/focus_and_input_text...),
  CLI flag --type (not --server)
- get_ui_hierarchy text/index target resolution for the flash skill

S10 headless findings (documented, route adjusted):
- headless composes no preset registry; its agent runs the dsh-base base
  toolset (24 tools) and root-inserted plugin tools do not reach it -
  verified over four runs including a registry insert and a dsh-base group
  insert; headless is therefore not a valid S10 test surface
- web profile composition verified clean with the spike overlay (registry
  default=autogamer, preset row, plugin layer all present, exit 0) - S10
  four-point validation moves to the web profile
- pi-ai posts to <baseURL>/chat/completions without /v1: LM Studio needs
  baseURL .../v1 (found via a logging proxy; llama.cpp masks this because
  it serves the un-prefixed path)

New spike assets: mock MCP action server (canned success, isolates model
behavior), logging proxy (dump-proxy.mjs), headless overlay + cmd shim
(msys positional quirk workaround). 13 vitest cases green.
- 实测:artemis-installed 与 helper 同因红(project-venv 形态 PATH 无 artemis 可执行),
  只修一项预检仍整体红 → 范围扩到 helperCheck,共用 uv run --project <repo> --no-sync 探针
- adb-devices 列出 4 个 serial,从预检层独立佐证 G75
- 按 R22:本条方案提交先于实现提交
…Check)

提供方有两种安装形态,旧预检只认第一种:uv tool list 里没有 artemis 就判未安装,
helper 项又假定 PATH 上有可执行文件;而 editable checkout(uv sync)两者都不留,
导致真机在场也两项同红。改共用一条只读探针 `uv run --project <repo> --no-sync artemis …`
(--no-sync 刻意:预检不得改动提供方环境),命中即标出是哪种形态答的。

- firstArtemisLine 剥 box-drawing 后取最长含名行:真 banner 首行是边框也含 ARTEMIS
- 测试夹具对未登记命令默认 {code:0},不显式脚本化 venv 探针会让旧失败用例假绿,故同批改 2 条
- 新增 5 锁;变异 M1/M2/M3 逐条证伪(红点 :145 / :166 / :218),还原后 diff -q 相同
- 真机预检:artemis-installed 转 PASS,helper 换成真红(提供方要求 --serial)→ 另立 G79
ready/degraded 两个 Panel 都写着 `artemis run "…"`,而 run 随智能栈已删;更糟的是这条 stale
文案被既有测试断言锁着(谁改谁红),所以旧断言必须同批改。
改指 `artemis mcp --type adb`(收缩后插件就是这个动作面唯一消费方)。

- 新增 test_cli_doctor_footer_advertises_only_existing_commands:允许名单不手抄,
  用 typer.main.get_command(app).commands 当场取已注册集,正则扫输出每个 `artemis <命令>` 断言子集,
  ready 与 degraded 两路都验;命令面以后再变锁自己跟上
- 变异:把旧文案注回两处 Panel(锚点命中 2)→ 新锁与改过的旧锁双双转红,还原后 diff -q 相同
- py 全套 447 → 448 passed(+1);ruff 双项与 quality_ratchet 通过;文档双门禁 rc 0
Step 4 收益优先级调研(四路:本仓改动面审计 / Android-adb 一手口径 / DSH 交付路径 / 提供方形状)
改变了排序,其中两条是收回我自己写错的结论:

- 收回「dsh plugin remove 只删依赖、CLI 侧不存在卸载命令」:operations.ts:88-97 的 reconcile()
  在 pnpm 成功后就把消失的名字从 dsh.profile.bundles 剔除 ⇒ G71 有 0 审批路径,成本重评
- 收回 G74 里「{index} 改吃 parsed_bounds」:该字段早已被优先采用并有锁(apply.spec.ts:62、
  dedup.spec.ts:17)—— 写待办前没核现状,正是 R25 禁的事
- 新立 G80:rulesInjector 读 <repo>/mcp_server/rules.md,本分支无此文件而失败即 warn-skip
  ⇒ rules 注入在真环境静默 no-op,Phase 5「已落地」只在 fake-repo 夹具下成立
- 口径纠正:正文里的「13 工具动作面」不准,实测与 fixture 都是 15(13 是 ACTION_TO_PY_TOOL
  的非 null 映射数);pre-contraction ref 现已不可解析,等价内容经 main 取得(G72 第三条路)
- §九 补 adb 身份约束:只有 emulator-5554 的 console↔adb 端口配对有文档化身份含义,
  钉 TCP serial 的白名单会静默丢设备;§十 落 HOME 由框架消化、forward 属 adb server、
  scrcpy 的 serial+port 精确删模式
- 门禁自测自身的腐烂点(google#81):注入变异把悬空 id 写死成字面量 G77,而该号本轮被登记 ⇒
  变异变成哑弹、自测判「尺子瞎了」。改为从登记表推导 max+1 并加两道自证;selftest 复 3/3
- G71 走 CLI 零审批路线执行完毕(profile 层):remove autogamer-device → add link:dsh-autogamer,
  bundles 现为新名、link 可解析;判据用宿主自己的——不带 --patch 的 --dump-config rc 0 且合成出
  "# == dsh-autogamer" 层。剩 R7/R9 会话取证未做(需启宿主),故 G71 不结案,Done means 逐项标注
- 收回 §八 两条未证:remove_bundle 死链可行性已本机执行;G59「常驻失败」定案为宿主 schema dumper
  走 preset 子项的固有行为(carrier 错 5 条里 4 条属宿主自带 preset,第 5 条才是我们;
  headless/desktop 同 rc=1 但 0 条 carrier)
- 摘除 pnpm 删依赖后残留的死 junction(G49 记过的致害形态)
- G79 / G80 方案定稿(先于实现提交,R22):helperCheck 非空才追加 --serial;
  rules 注入返回值改结构化结果并经 autogamer_diagnose 暴露告警,不新增第 8 项阻塞预检
G79:PreflightInput 加必填 defaultDeviceSerial(必填是刻意的——可选就是这次漏传的成因),
helperCheck 给 PATH 与 project-venv 两条形态都追加 --serial;提供方没有 --all,
多传输同机时不点名设备就被直接拒绝。
G80:setupRulesInjection 返回值由 boolean 改为 injected/disabled/no-repo/missing-file/
read-failed/empty,index.ts 记下它,autogamer_diagnose 在非 injected 且非 disabled 时
往 recommendations 追加写明原因与路径的告警;没有新增第 8 项阻塞预检(七项 id 集有锁,
缺 rules 不该阻断设备动作),disabled 保持安静。

验收:
- 真机对照:defaultDeviceSerial="" → FAIL helper(仍是那句 fleet 拒绝);
  ="127.0.0.1:16384" → PASS helper 且七项 overall ok: true —— 这条链上第一个全绿预检
- G80 live:对本仓真跑注入得到 missing-file(不再是一个被丢掉的值)
- 插件 32 files / 293 passed / 1 skipped(+4 锁);变异 MA/MB/MC 分别红 1/1/2 条
- 文档双门禁 rc 0;CHANGELOGS google#82

附带发现另立 G81 不在本条顺手修:tsconfig 的 include 只含 src ⇒ 插件 32 个 spec
从未被类型检查,纳入 tests 一次 tsc 实测 144 条错误(apply.spec.ts 独占 45)。
启 `dsh --profile web --no-open --port 8090`(URL 带 token,不入库),在 agent 内置浏览器读官方管理面:
- 插件页:已安装 1 = AutoGamer 设备执行 v0.1.0 / dsh-autogamer,启用为开
- 详情页:包含的组件 共 2 个 · 2 运行中(dsh-autogamer + preset-autogamer),无 error
- 预设菜单:出现 AutoGamer 且可选为新会话预设
⇒ Done means 的「装成且无 error」转 [x];「工具真在 Agent 面上」保持 [ ]——
右侧栏无会话前工具清单,而 G30 的教训是注册成功不等于 Agent 看得见,故 G71 不结案。
另按 R24 记一条口径:宿主 stdout 仅 3 行无报错,但插件 logger 不进 stdout,
所以「日志没报错」既不能证明也不能否证,本轮结论全部只取管理面读数。
用户问「是不是另装了一个 dsh」,查清结论相反但更要紧:
- 排除安装:本机只有一个 dsh(D:\DeepSeek Harness,exe 构建 Sep 29 18:34,CLI 0.2.0-rc.2),
  where.exe dsh 与 npm/pnpm 全局 bin 均无第二个;我起的进程 22060 的可执行文件就是该 app
- 真问题在 profile 层:本机四个 profile 各自独立。web 已修;headless 仍挂着与 G71
  同一个旧名死链(autogamer-device -> 已无 package.json 的目录);desktop 没装本包,
  而用户 22:56:27 起的 5 个 app 进程走的正是 desktop(其 cordis.yml 同时被重写)
⇒ Step 7 的"生效"读数不适用于用户日常环境;G71 行与 §零 都加了范围限定
本轮只落账,不改任何环境:
- §零 新增 Step 8:结案 4 条 / 未关闭 7 条的位置索引;已验证到 live 的清单;
  仍未验证的清单;按"先不动"保留的现场(desktop 未装、headless 旧链未清、web 安装未回滚、
  8090 宿主仍在跑、wire 取证未开始)
- §零 新增「待用户裁定清单」8 条,每条只写决策点 + 我的建议,正文指向对应 gap 行(不重述)
- G71 在办条目补「范围」段:条目当初只写 web,本机四 profile 独立;目标 profile 由待裁第 1 条定,
  定了之后 What/How/Verify 三条原样适用
headless dead-link removed (pnpm stale junction reaped), web profile
model endpoint synced from desktop (local -> 127.0.0.1:1234/v1,
qwen3.6-35b-a3b-mtp), and the last G71 done-mean proven: a fresh
AutoGamer-preset session exposes all 7 plugin tools via the
approval-free cordis_inspect_query inspector. Prose moved from
TODO section 4 to CHANGELOGS google#83; registry open count 7 -> 6; both
doc gates green (size table refreshed).
…p 10)

Research (upstream presets + tool-cordis) showed cordis_inspect_query is
only mounted by the shipped cordis preset, so the previous session answer
was the model restating its context, not inspector evidence. Re-ran the
G71 evidence with the S10 wire rig: dump-proxy (1235->1234) + wire-tools
counted tools=12 on the AutoGamer session request, all 7 plugin tools
present plus 5 host rows matching the shipped preset composition.
Endpoint restored to 1234, proxy stopped, host restarted. TODO Step 9
claims struck through per R28; CHANGELOGS google#83 acceptance rewritten as a
re-runnable capture procedure.
…rst)

User approved adding @deepseek-ai/dsh-tool-cordis so AutoGamer sessions
gain cordis_inspect_list/query (R9 inspector currently unavailable on
that face). Plan: add the row to both shipped and live patches (locked
pair), extend deliveryShape lock, R6 + offline tests now, live wire
re-capture (tools=14) pending model endpoint readiness.
Adds the same no-config row the shipped cordis preset uses, so AutoGamer
sessions gain cordis_inspect_list/query (the R9 inspector their face
previously lacked entirely). Both delivery shapes updated in lockstep and
deliveryShape.spec now locks the row (G37/G45 lesson). vitest 32 files /
293 passed / 1 skipped; R6 dump-config rc 0 with zero unmatched and the
row visible in the composed tree. Live wire re-capture (tools=14) pending
model endpoint readiness per TODO Step 11.
…p 12)

Brings the branch-local task provider back onto dsh-plugin-contraction
per user ruling (main stays frozen; provider must live here):
mcp_server/** (5 mobile_* tools + rules.md), artemis sdk/agents/tools
smart stack, runtime trace/daemon files, missing utils/diagnostics, and
third_party closure (~250 files, verbatim from main).

Survivor-file compatibility, all verified by the boundary guard:
- artemis/__init__: PEP 562 lazy Agent/Builders from artemis.sdk
- artemis/config/__init__: lazy provider surface (agent/llm/output
  groups) so the action face still imports without the LLM stack
- artemis/runtime/__init__: re-export daemon five + ProcessSupervisor
- artemis/mcp/action_types: ObserveResult merged back verbatim

config/artemis.jsonc restored as tracked (localhost LM Studio default,
no credential material). pytest 685 passed / 1 skipped (baseline held,
boundary params grown and green); ruff check + format clean; provider
face = mobile_run_task / mobile_manage_task / mobile_inspect_trace /
mobile_get_device_state / mobile_diagnose, matching the plugin adapter
call sites verbatim.
Deletes 37 zero-reference files the G72 closure restore dragged back:
old console build assets (showcase_ui), seven legacy CLI commands, the
artemis-client residue world (interfaces/sdk), legacy shims
(main/_legacy_imports/__main), the superseded standalone
mcp/action_server, telemetry, cloud driver, config/core, and
third_party test helpers. Engine-referenced old action-face execution
layer (action_executor/specs/session/names/observation) kept - grep
proven live (flash/validator/operator import them).

Verified: mcp_server.server + artemis.sdk imports ok, provider face
unchanged (5 mobile_* tools), pytest 664 passed / 1 skipped (boundary
params shrank with the deleted files), ruff check + format clean.
Boundary statement recorded: py = vendored provider module (frozen from
upstream), TS plugin = main stack (D18).
Lands the analysis as a spec, not code (R22 spec-first):
- docs/migration/ts-fullstack-spec.md: terminal state (4 done criteria),
  capability migration matrix (upstream ARTEMIS -> TS carrier -> judge),
  four phases T1-T4, contract-boundary items to measure first, risks
- registry: D19 (tightens D18) + G82-G85 (one per phase); T1-T4 noted as
  phase names, not ids
- TODO: D19 row, four gap rows, new section 7 flow with checkboxes and
  per-phase gates; terminal-state summary in section 0
- README/AGENTS: spec registered in the nav and doc map; section 2 amended
  (py reuse is now transitional, D19 sets the terminal state)

Gates: registry exit 0 (ids 143), tables exit 0 after --refresh-sizes
(13 size rows), ruler selftest fires on all 8 injected defects, ruff clean.
User requirement folded in: the repo, though forked from google/artemis,
must retain nothing but LICENSE - inspiration only, independent
reimplementation.

- spec gains section 8 (provenance & licensing) with measured facts:
  root LICENSE is Apache-2.0; third_party/mobile_use carries its OWN
  LICENSE+NOTICE from a different upstream; 319 tracked files carry
  Google LLC headers; upstream remote points at google/artemis. Hard
  constraint recorded: TS rewrite borrows DESIGN only, never TEXT
  (verbatim copy = retained derivative content) - applies across T1-T3.
- terminal-state criteria 4 -> 5 (provenance zero added)
- registry: D20 + G86 (independent axis from D19/G85)
- TODO: D20 row, G86 row, provenance checklist line in section 7

Two questions explicitly left to the user (not decided here): git history
handling and the upstream remote. Until decided, D20's criteria cover the
working tree only, and the spec says so.

Gates: registry exit 0 (ids 145), tables exit 0 after refresh, ruler
selftest fires on all 8 injected defects.
giveUphope added a commit to giveUphope/autoGamer that referenced this pull request Oct 13, 2026
Two structural entries let work leak toward the third-party upstream: the
`upstream` remote (github.com/google/artemis) and an unset push default.
Both addressed: `upstream` removed, `remote.pushDefault=origin`.

Also records a real incident: an accidental cross-repo PR exists at
google#182 (head giveUphope:legacy-fork-v2-dsh-plugin -> base
google/artemis:main, OPEN). Closing it needs the user - this token has no
write access there, `gh pr close` returns 403.

And records my own error: `gh pr list --repo <this repo>` only lists PRs
whose BASE is this repo, so it cannot see a PR FROM here TO upstream - yet
I used it to claim "no open PRs" and to justify the rename as harmless.
AGENTS.md now carries the correct predicate.
giveUphope added a commit to giveUphope/autoGamer that referenced this pull request Oct 13, 2026
Removes the remaining path by which work could reach google/artemis:

- .githooks/pre-push refuses any remote whose URL is not the allowlisted
  origin (giveUphope/autoGamer); a non-zero exit aborts the push and no
  refs are updated.
- core.hooksPath=.githooks makes it effective (a versioned hook alone is
  not installed - the old fork learned that with its pre-commit hook).

Verified by an A/B against a throwaway local bare repo: a non-allowlisted
push is blocked (exit 1 + BLOCKED), and the same push with --no-verify
succeeds - so the evidence shows the hook is what blocked it.

Honest limits recorded in AGENTS.md and docs/TODO.md: pre-push runs only
after git contacts the remote and lists its refs, so it stops the push,
not the connection (an unreachable target fails during connect and never
reaches the hook); and PR creation is outside any local hook's reach.

Also records that google#182 is still OPEN - the reported close
did not take effect, re-checked with the same query.
@giveUphope giveUphope closed this Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant