Generalized from a long hardened-target engagement. Additions target the failure
modes that cost the most time and are not yet covered; nothing here is sample-specific.
New discipline
- SKILL.md: a third override rule -- deliver in the environment the request requires.
A root-only or live-instrumentation result can look like success while failing the
actual requirement (unrooted, installable, self-contained). Also adds two classify
questions: what form the deliverable must take, and which architecture/library is
actually executing.
- long-task-discipline.md: "the most expensive drift" -- a constraint table (privilege,
modification form, ABI, network, persistence, distribution), the checkpoint question,
and how to report a privileged workaround as a fallback rather than a deliverable.
Plus "bound every wait": timeouts on every command, budgets calibrated from
measurement so slow and hung are distinguishable, and sampling an observable you can
inspect instead of sleeping blind.
- environment.md: look at the screen instead of driving blind; which of the two screen
evidence kinds is trustworthy (an empty accessibility tree is common for
runtime-rendered UI, where the image is primary rather than a fallback); emulator
console control and adb recovery; preflight before every experiment block.
- native-and-so.md: what the device claims vs what the package manager chose vs what is
actually mapped; translation layers and their consequences for hooks and native
checks; fat-APK ABI selection.
- runtime-data.md: when the app rewrites your edit (server refresh vs local
regeneration), why chmod/chown cannot work because the app deletes and recreates the
file, and the immutable-attribute fix; decoding a value that only looks encrypted.
- framework-runtimes.md: string encoding traps (a UTF-8 scan of a UTF-16 snapshot
returns nothing and reads as "the strings were stripped"), and non-ASCII obfuscated
identifiers -- resolve by shape, not by name.
- pitfalls.md: P23 the fix lives in a file the app rewrites; P24 the artifact changed
but the wrong one is executing; P25 "search found nothing" treated as "not there".
New scripts (parameterized, path-agnostic, all with explicit timeouts)
- preflight.py environment check: device, root, ABI/translation, clock skew, leftover
proxy/forwards, dead device server, chosen ABI, free space
- lib_map.py what is actually mapped into a live process: path, base, architecture
from the ELF header, and whether it came from the APK or was
materialized at runtime
- blob_decode.py search (don't guess) the framing of a stored value, then re-encode the
edited payload with the same parameters
- snap.py bounded burst screenshots + control-tree capture, with a stall detector
and an explicit verdict on whether the tree is usable
All four were exercised against a live device while writing: preflight correctly
flags multi-device ambiguity and detected the ARM translation layer; lib_map read real
ELF headers and identified the translation shim; blob_decode round-tripped a
rotation+deflate+base64 blob at realistic size in ~0.2s; snap detected a stalled screen
across three hashes.
13 KiB
Long-Task Discipline
Load this when a task is likely to run long: many rounds, many experiments, or a conversation that will exceed what can be held in context. Also load it before resuming a task someone else (or a past you) started.
The failure mode this file prevents is not tooling — it is judgement drift. Over a long task, three things reliably go wrong, and each is expensive in a way that is invisible while it happens:
- You re-derive what you already proved. Rounds get spent rediscovering a boundary or a mechanism that was established earlier and then lost.
- You re-walk a route that was already excluded. The exclusion is real, but the reason has been forgotten, so the route looks promising again.
- A wrong conclusion keeps steering. An early mis-attribution is written into your mental model and silently removes good options, or props up bad ones, for a long time afterwards.
All three come from the same cause: conclusions living in context instead of on disk.
Keep a live record, not a log
Maintain one artifact — a single file — that is the task's authoritative state. Update it as things are learned, not at the end. What matters is not size; it is that every entry is actionable later.
Keep these sections:
| Section | Contents | Why |
|---|---|---|
| Operating rules | things that must be true every run (how to install, how to launch, how to capture) | prevents repeating a mechanical mistake |
| Confirmed facts | each with the run that proved it | the basis everything else builds on |
| Refuted conclusions | what you believed, why it was wrong, what replaced it | stops a dead idea from coming back |
| Dead routes | ruled out, with the evidence | the single biggest time saver |
| Open questions | what is still unknown | keeps the next step honest |
| Environment | device, ports, tool paths, credentials locations | avoids re-discovery |
Two rules make this file worth having:
- Every claim carries its evidence. "The shell rejects this" must include the run that showed it, including the exact failure text. An unattributed claim is how a false conclusion gets adopted later.
- Refutations are first-class entries. When you find that an earlier conclusion was wrong, record both the wrong conclusion and why it was wrong. Deleting the mistake loses the most valuable thing you learned.
Grade your own conclusions
Label every non-obvious statement with its strength, and never let a weaker label inherit the authority of a stronger one:
| Grade | Meaning | May it justify a decision? |
|---|---|---|
| Observed | reproduced it, with the exact command and output | yes |
| Inferred | follows from an observation, but the step is reasoned | provisionally |
| Hypothesis | plausible, untested | only as something to test |
| Refuted | tested and found false | never — keep it only as a warning |
Most long-task damage comes from hypotheses drifting upward into "facts" simply by being repeated. If you catch yourself referring to something as established, check the grade in the record. If it is not observed, go observe it or label it again.
Single-variable discipline across the whole task
The most common source of a wrong and durable conclusion is a compound experiment: two changes, one failure, one invented explanation. Because the failure is real, the explanation feels earned.
Consequences to enforce:
- When a route is about to be abandoned, re-read why it was abandoned. If the evidence was compound, the abandonment is not yet justified — re-run it single-variable before writing it off.
- Keep a semantic control in your pipeline: a build with no changes, run through the same install-and-launch path. If the control fails, nothing else you measure is meaningful.
Guard against drift at natural checkpoints
Do these cheaply, and only when they can change a decision — not as ceremony.
- Before starting a new experiment: read the refuted-conclusions and dead-routes sections. If your plan appears there, stop and read why.
- Before declaring progress: ask what the user-visible outcome is right now. An internal signal improving is not progress (see P19). Re-state the actual target.
- After any surprise: write it down immediately with evidence, before you have a theory. Theories written after the fact are hard to distinguish from observations.
- When context feels long: prefer writing to the record over re-reading conversation. The record is what survives; the conversation is what gets truncated.
- When resuming: read the record first, and treat anything not in it as unknown, even if it feels familiar.
The most expensive drift: solving it in an environment the deliverable will never see
There is one drift in this domain that costs more than all the others, because it produces a result that looks like success and is not one.
A runtime-only result is easy to obtain and easy to mistake for a finished artifact.
Editing a data file, hooking a live process, blocking a hostname, holding a proxy open — each of these can make the app behave correctly on the machine where you did it, with no repackaging required. It is often the fastest path to a visible win. It is also frequently not the deliverable at all, because the requirement was never "make it work here".
The constraint axes that decide this — check every one against the original request, and write down the answer as a testable sentence before you start:
| Axis | The question | Why it silently invalidates work |
|---|---|---|
| Privilege | must it run unrooted? | a rooted-only result cannot be given to a normal user at all |
| Modification form | must it be a rebuilt/installable artifact, or is a live-instrumentation result acceptable? | hooks and data edits do not ship inside an APK |
| ABI / device class | which ABI, which device family? | an emulator-only or x86-only result is not evidence for a physical ARM device |
| Network | must it work online? | a fix that depends on being offline or on a host proxy fails the moment it is used normally |
| Persistence | must it survive restart, upgrade, and a fresh install? | in-memory and session-scoped state evaporates |
| Distribution | does the shipped file have to be self-contained? | anything that needs a helper on the machine is not a shippable artifact |
Checkpoint question, asked at the same moments as the drift checks above:
If I handed over exactly what exists right now, would it satisfy the constraint sentence I wrote at the start?
If the answer is no, you have made real progress on a component, and that is worth stating as such — but it is not the task. Do not let "it works" stand in for "it works under the constraint".
What to do instead of over-claiming. Split the result into two explicitly labelled parts:
- What is achievable inside the constraint, and how far along it is.
- The unconstrained workaround (needs root / needs a host / needs a proxy), stated as a deliberate fallback, with its requirements made obvious to the reader.
A privileged workaround is genuinely useful — it can be the difference between using the app and not. It becomes a problem only when it is presented as the deliverable. Report it as a fallback, keep the constrained goal open, and say plainly which one you have.
Also watch the inverse: if the constraint is "must be a rebuilt artifact", do not let a working runtime result quietly close the investigation. Use it for what it is good for — it proves the mechanism and identifies the exact code or data to change — then port that finding into the artifact. The runtime result is the map, not the destination.
Bound every wait
A long task stalls in ways that produce no information and consume the most valuable resource you have: wall-clock time and the next person's patience. Three habits prevent almost all of it.
1. Every command has a timeout, and its absence is the bug. A helper that shells out without a timeout can hang forever, and the failure presents as "the task stopped making progress" rather than "this call blocked". Pass an explicit timeout on every subprocess, every HTTP request, every device call. When one expires, the result is unknown, not failed — record it that way and re-check state before retrying.
2. Calibrate the expected duration from measurement, not from a guess. A timeout only means something if it is set relative to how long the operation should take, because that is what makes "slow" and "hung" distinguishable. Guessed budgets are either so short that healthy work gets killed, or so long that a stall looks like patience.
So: measure once, write it down, then derive the bound.
| Operation class | How to bound it |
|---|---|
host-side tool (baksmali, apksigner, a compiler) |
seconds to low minutes; time it once, then set roughly 3–5× observed |
device shell call (adb shell …) |
seconds — except the first su after a reboot, which can prompt and block |
| install / push of a large artifact | minutes; scales with size, not with the app's complexity |
| app cold start to first frame | tens of seconds — longer with a packer, which does real work before your code runs |
| a step needing a human or an external service | unbounded in principle — do not wait at all (see P22) |
| waiting for an on-screen state change | bound it and sample it (point 3) |
Record the observed durations in the live record. They are exactly the kind of mechanical fact that gets re-derived painfully after a context loss, and having them turns a later timeout into an interpretable signal instead of a mystery.
Exceeding the expected window is itself the finding. It tells you something about state — the device is wedged, the app never reached that phase, the action never happened — and that is a different investigation from waiting longer. Re-check state instead of extending the deadline.
3. Every wait has a deadline, an observable to sample, and a look. "Wait for the operation to finish" is not a plan. Name the observable, how often you will sample it, how long you will sample before giving up, and what you conclude on expiry:
waiting for: <observable> e.g. the overlay is gone / the counter incremented
sample: <interval + how> e.g. every 15 s: screenshot + current window focus
give up at: <deadline> e.g. 150 s
on expiry: <what I conclude> e.g. "did not complete within the window" -- not "cannot work"
Sample with your eyes rather than sleeping blind. When the step is gated on something visible — a
screen, a dialog, a progress indicator — capture it and look at it instead of sleeping through the
interval. A fixed sleep either wastes time or measures the wrong instant; a look tells you what
actually happened, and byte-identical consecutive samples tell you nothing is going to change
(environment.md §look at the screen). This is the cheapest way to avoid spending rounds on a state
that was never going to arrive.
Three rules that follow:
- A deadline that passes is a measurement, not a verdict. "Still not finished after N seconds" tells you about latency and reliability. It does not tell you the approach is impossible, and writing it down as impossible is how a viable route gets discarded.
- Never busy-poll a long job while you have independent work. Start it, do the other thing, and collect it when it settles. Polling wastes the same resource the task is already spending.
- Prefer a bounded observable over a fixed sleep. Poll the state you actually care about, with a cap — and prefer a sample you can inspect over a duration you hope is right.
Handover
A handover is the record plus three things, written for someone with no memory of the task:
- Operating rules first — anything mechanical that silently wastes time if unknown.
- Honest current state — including what is not achieved. An optimistic summary costs the next person far more than an accurate one, because they will build on it.
- The next best step, and why — plus the dead routes, so they do not start there.
Two things belong in every handover, because they are the hardest to recover:
- What you got wrong, with the evidence that corrected it.
- What you never actually verified, stated plainly. "Never confirmed" is more useful than silence.
Avoiding the opposite failure
Discipline can itself become a burden. Guard against that too:
- Do not log everything. Record only what would change a future decision or prevent a repeated mistake. A record nobody reads is worse than no record, because it looks like coverage.
- Do not treat the record as authority. It is a summary; the environment is ground truth. If the record and a fresh observation disagree, re-test — do not defend the record.
- Do not let grades calcify. A hypothesis that becomes testable should be tested, not archived.
- Do not let the record's structure dictate the work. If a section is empty because it is not relevant to this task, leave it empty.