DEV Community

Quinn Zhu
Quinn Zhu

Posted on

Day One: Keep the Agent Read-Only Until Tests Run

Agents will invent your stack on day one. You should treat every invention as untrusted. Keep the agent read-only until local tests pass. That is the entire onboarding rule for juniors.

You clone a repo and feel pressure to ship. A chat box offers a complete local setup. It guesses the package manager and the test runner. It may also invent ports, paths, and env keys. Those guesses look helpful and they are often wrong. You apply a diff you cannot explain. Your first PR then teaches the team a bad habit.

The rule in one line

No AI writes until you run the real tests. You may ask questions, but you may not patch files. Record observed facts in a small YAML file. Do not let the model draft that file.

This workflow is a proposal for a first clone. Label every script below as unexecuted until you review it. You still own the merge button after the gates pass.

Gate 1: Hand-write a repo fact file

Open CI first. Then open the README. Copy commands you can see, never ones you imagine. Create .repo-facts.yml at the repo root. Fill only fields you verified in the tree. Leave a field blank if you have not seen it. A blank field is safer than a confident lie.

# .repo-facts.yml
# Hand-written on day one. Do not auto-generate.
package_manager: ""      # npm | pnpm | yarn | bun | other
install_command: ""      # copied from CI or docs
test_command: ""         # copied from CI, not from chat
lint_command: ""
dev_command: ""
runtime: ""              # language + version if documented
default_branch: ""
ci_file: ""              # e.g. .github/workflows/ci.yml
forbidden_paths:
  - ".env"
  - ".env.*"
  - "secrets/"
  - "credentials/"
facts_verified_at: ""    # ISO date you filled this
Enter fullscreen mode Exit fullscreen mode

Now hunt those values with commands, not chat.

  1. List workflow files in the default CI folder.
  2. Search for the real test invocation next.
  3. Print the default branch from origin metadata.
  4. Confirm the lockfile that actually exists.
  5. Paste only what those commands printed.
ls .github/workflows 2>/dev/null || ls .gitlab-ci.yml Makefile 2>/dev/null

rg -n "npm test|pnpm test|yarn test|pytest|go test|mvn test|cargo test" \
  .github Makefile package.json pyproject.toml go.mod 2>/dev/null

git remote show origin | rg "HEAD branch"

ls package-lock.json pnpm-lock.yaml yarn.lock bun.lockb \
  poetry.lock Cargo.lock go.sum 2>/dev/null
Enter fullscreen mode Exit fullscreen mode

You now have a fact file the agent cannot own. If CI and README disagree, stop. Ask a human. Do not average the two answers.

Gate 2: Queue questions instead of diffs

Create .day1-questions.md with your real unknowns only. One question per heading. One file path per question. You must open that path before you ask.

# Day-one question queue

## Q1. What installs dependencies in CI?
- Opened: .github/workflows/ci.yml
- Status: unanswered
- Answer:
- Verified in tree: no

## Q2. Which test command must stay green?
- Opened: package.json (scripts.test) and ci.yml
- Status: unanswered
- Answer:
- Verified in tree: no

## Q3. Which paths must never appear in a diff?
- Opened: .gitignore and .repo-facts.yml
- Status: unanswered
- Answer:
- Verified in tree: no
Enter fullscreen mode Exit fullscreen mode

Use the queue in this exact order.

  1. Pick the top unanswered question.
  2. Open the cited file in your editor.
  3. Ask the agent in read-only mode.
  4. Verify the answer against the open file.
  5. Mark the question answered or still blocked.

Never skip step two. The file is the source. If the model says UNKNOWN, you stop. You do not let it fill the gap.

Here is a read-only prompt template. It is unlabeled as executed. Paste it only after the file is open.

You are read-only. Do not propose file patches.
Do not invent commands missing from .repo-facts.yml.
Question: Why does CI set CI=true for the test job?
I opened: .github/workflows/ci.yml lines 40-62.
Answer only from that file. Say UNKNOWN if missing.
If you need another file, name it. Do not guess.
Enter fullscreen mode Exit fullscreen mode

Keep secrets out of the prompt. Do not paste .env. Do not paste tokens. Do not paste customer data. A question about a port is fine. A dump of credentials is not.

Gate 3: Unlock writes after a green run

The agent stays locked until tests actually run. You run the command from .repo-facts.yml yourself. A small script records the unlock stamp. Review this script before you chmod it.

#!/usr/bin/env bash
# scripts/day1-unlock.sh
# Proposed local gate. Review before you trust it.
set -euo pipefail

FACTS=".repo-facts.yml"
STAMP=".ai-write-unlock"
QUESTIONS=".day1-questions.md"

if [[ ! -f "$FACTS" ]]; then
  echo "missing $FACTS — write facts by hand first" >&2
  exit 1
fi

need() {
  local key="$1"
  local val
  val="$(rg -N "^${key}:" "$FACTS" | sed 's/^[^:]*:[[:space:]]*//; s/"//g')"
  if [[ -z "$val" ]]; then
    echo "blank field: $key" >&2
    exit 1
  fi
  printf '%s' "$val"
}

install_cmd="$(need install_command)"
test_cmd="$(need test_command)"
branch="$(need default_branch)"

if [[ -z "${install_cmd}" || -z "${test_cmd}" ]]; then
  echo "install_command and test_command must be filled" >&2
  exit 1
fi

current="$(git rev-parse --abbrev-ref HEAD)"
echo "current branch: $current (default is $branch)"

if [[ ! -f "$QUESTIONS" ]]; then
  echo "missing $QUESTIONS — queue your unknowns first" >&2
  exit 1
fi

if rg -q "Status: unanswered" "$QUESTIONS"; then
  echo "unanswered questions remain in $QUESTIONS" >&2
  exit 1
fi

echo "running install: $install_cmd"
eval "$install_cmd"

echo "running tests: $test_cmd"
eval "$test_cmd"

date -u +"%Y-%m-%dT%H:%M:%SZ" > "$STAMP"
echo "writes unlocked at $(cat "$STAMP")"
echo "AI may edit only after you re-read the fact file."
Enter fullscreen mode Exit fullscreen mode

Make it executable only after you read every line.

chmod +x scripts/day1-unlock.sh
./scripts/day1-unlock.sh
Enter fullscreen mode Exit fullscreen mode

Add the stamp to .gitignore. The stamp is local proof, not a commit. You still write the code. The stamp only means tests ran on your machine.

.ai-write-unlock
.repo-facts.yml
.day1-questions.md
Enter fullscreen mode Exit fullscreen mode

Keep .repo-facts.yml untracked if it contains machine-specific paths. Commit a .repo-facts.example.yml if your team wants the template.

Decision table for the first ticket

Use this table before you accept any model output.

Situation Agent mode Your next action
Fact file still has blank required fields Read-only Hunt CI and docs
Question queue has an unanswered item Read-only Open the cited file
Model invents a command not in facts Reject Re-copy from CI
Install fails on a missing lockfile Read-only Ask a human
Tests fail on a clean clone Read-only Do not generate fixes yet
Tests pass and queue is empty Writes allowed Touch one file per turn
Diff includes .env or secrets Reject Reset the working tree
You cannot explain the diff aloud Reject Revert and re-read

One file per turn is enough on day one. Broader refactors wait for a named ticket and a reviewer.

After unlock: a tight write loop

Writes are not a blank check. Keep the loop small.

  1. Restate the ticket in one sentence.
  2. Name the single file you will change.
  3. Ask for a patch against that file only.
  4. Read the patch as if a stranger sent it.
  5. Run the recorded test command again.
  6. If tests fail, revert. Do not stack guesses.
git checkout -- path/to/the/file
./scripts/day1-unlock.sh
Enter fullscreen mode Exit fullscreen mode

If you cannot name the file, you are not ready. Go back to Gate 2. Ask one more question. Stay read-only.

A short explain-back note belongs on the PR. Three sentences. No model prose.

What changed: trimmed the timeout in worker/retry.ts.
Why: CI already sets the same timeout in ci.yml.
How I know: I re-ran `pnpm test` after the edit.
Enter fullscreen mode Exit fullscreen mode

If you cannot write those three lines, drop the patch. The agent did the thinking. You did not.

Where a free local helper fits

Gate 2 needs answers, not patches. A local helper can sit on that question queue. Keep prompts short and file-scoped. Do not upload the whole tree on day one.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is an open source coding assistant. The operator notes free model access and a free server option. Use that pair for read-only questions if it fits your team rules. It does not replace the hand-written fact file. It does not unlock writes. You still run the tests.

Skip any helper that wants secrets in the prompt. Skip any helper that drafts .repo-facts.yml for you. The value is the gate, not the brand.

Limitations

This gate cannot close if CI has no tests. A green local run is not a design review. The fact file goes stale after the next refactor. You must refresh it when install or test commands change. The unlock stamp is easy to fake. A determined person can skip the script. The method only works if you refuse to skip it.

Eval of install_command is a sharp edge. Only paste commands you copied from CI. Never paste a command the model invented. If the command looks like a curl-pipe installer, stop. Ask a teammate.

Windows shells will need a port of the script. The YAML fields stay the same. The rule stays the same.

Who should skip this

Skip this if you already own the stack. Skip this during a real production incident. Skip this if your team forbids local models or extra YAML files. Skip this on a throwaway prototype with no shared CI. Staff who wrote the repo do not need a passport. Juniors joining a brownfield service do.

Close

Your first job is not a clever patch. Your first job is a true test command. Keep writes locked until that command is green. Then change one file. Then explain the change in three lines. That is how you join a repo without inheriting an agent's guesses.

Top comments (0)