Skip to content
resoluna.

How I work with AI

I own the judgment, AI owns the implementation

The split is the same on every project: I define the problem, list the constraints, make the trade-offs and accept the result; Claude Code turns the decisions into code. This is not “ask AI to build an app”. I work out what I want and what I don’t before handing over a spec. AI writes fast, so the bottleneck is never the code. It is the judgment.

Requirements come from the room, not from an imagined user

The first version of the Earth Day Six Thinking Hats tool gave each student one fixed hat. After the first class I watched students without a hat tune out, and only then changed it to open participation with a Pass mechanism. I try to watch real users use the product, then turn what I see into the next round of requirements.

Questions with a right answer are not left to the model

On the zoo LINE bot, questions like ticket prices and closing days were at first answered entirely by GPT, and it improvised. The fix was to let code match rules first and have GPT only phrase the answer naturally. Whatever can be computed by a rule is computed by a rule; the model is kept for genuinely open questions.

Define what to look for before letting the model look

When using AI to analyse student answers after class, I did not ask the model whether the writing was “good”. I defined four dimensions first: does the yellow hat cite the white hat’s data, does the green hat answer the black hat’s obstacles, is the blue-hat plan concrete or a slogan, and does the whole chain hold together. The quality of the output depends on how clearly the question was asked.

Every decision leaves a record

Context, the options at the time, the call, the reasoning, and the condition under which it would change. These records give AI context the next time it picks up the work, and give me a way to audit my own judgment. The decisions page is compiled from them.

Collaboration replay

Every entry actually happened: what I decided, what AI did, what came of it.

  1. Taipei Zoo Course Assistant LINE Bot ·Environment landmines: runs locally does not mean it builds

    What I decided

    The app ran fine locally but Cloud Build failed. I pasted the failing section of the build log and asked the assistant to first establish exactly which packages were in conflict, rather than rebuilding the whole environment.

    What AI did

    Cursor pinned down the hard conflict between numpy==1.24.0 and seaborn and relaxed it to a range (>=1.22.4,!=1.24.0,<2); for the aenum compile error caused by leftover Python 2 syntax it added --no-compile to pip in the Dockerfile.

    Outcome

    Cloud Build passed and the bot went live on Cloud Run for the first time. The same aenum trap resurfaced in setup.sh four months later and got the same --no-compile fix.

    Lesson Use version ranges, not pins; read the error before touching the environment →

  2. Taipei Zoo Course Assistant LINE Bot ·The turning point: taking judgement back from GPT

    What I decided

    I noticed that for a verifiable question like 'what is on Saturday' GPT would pick the wrong weekday or drop courses, and two days of commits had gone into arguing with it in the prompt. I stopped patching the prompt and asked for Python to filter courses by weekday first, leaving GPT only to phrase the result.

    What AI did

    The assistant (Cursor) added detect_query_weekday and matches_weekday for weekday matching and pre-filtering, then fallbacks for when filtering fails, and went on to structure tickets, closures and hours into CSVs behind a single query_router.

    Outcome

    Deterministic queries no longer pass through GPT's judgement. The principle later spread across the project, including group ticket quotes (GPT only extracts identities, Python sums) and returning the environmental-education hours text verbatim.

    Lesson Questions with one right answer should never be left to a free-form model →

  3. Chinese Homework Tracker ·Carving the publishable slice out of a grade sheet

    What I decided

    The source was my running-grade sheet. I decided to take only the submission and correction columns, show seat numbers only, require no sign-in, and ship to three classes immediately.

    What AI did

    Gemini produced the Apps Script to those constraints: a doGet page, a parser for the three-row header that recognises only four assignment types, and a mobile-first card UI.

    Outcome

    Live immediately; used by students, homeroom teachers and the subject teacher across three classes.

    Lesson Say what must stay private before saying what to build →

  4. Earth Day × Six Thinking Hats ·Fixing a sync-state bug

    What I decided

    After switching to open participation, the Save button on the student screen got locked. I handed Claude the symptom.

    What AI did

    Claude traced it to subscribeHatReadiness mis-handling open-to-all mode plus a hatKey race condition, and fixed both.

    Outcome

    Save works again under open participation; the fix is in the commit history.

    Lesson When you change the flow rules, re-check every sync assumption built on the old ones →

  5. Earth Day × Six Thinking Hats ·Turning a teaching goal into UI

    What I decided

    I wanted green hat to respond to black hat's concerns, so I asked for the group's black-hat answers to be visible while answering green hat.

    What AI did

    Claude added a live-subscribed “our group's black-hat concerns” panel to the green-hat screen that updates as teammates save.

    Outcome

    Black→green response went from memory to something on screen, and later became one dimension of the post-class analysis.

    Lesson If you want to assess a behavior, first make it possible in the interface →

  6. Earth Day × Six Thinking Hats ·Changing product rules from classroom observation

    What I decided

    After teaching with it, I dropped one-hat-per-student for whole-group answering with Pass, split white hat into specific fields, and asked for answer editing in the teacher console.

    What AI did

    Claude fixed the answer buttons, split white hat into three fields, implemented Pass, and added answer editing to the console.

    Outcome

    Everyone has a task under every hat; skipped fields have an explicit way out instead of blocking the group.

    Lesson Let a real classroom test the role design of a prototype →

  7. Earth Day × Six Thinking Hats ·Post-class AI analysis

    What I decided

    I defined the frame: not AI detection, but whether yellow cites white's facts, green answers black's concerns, and blue is a concrete plan.

    What AI did

    Claude wrote analyze-708.js: read Firestore, compute completion rates, blue-hat consistency and action distribution in code, then hand stats and raw answers to Azure OpenAI for qualitative analysis.

    Outcome

    A report on 708's 28 responses with per-group coherence and three teaching recommendations.

    Lesson Let code compute the numbers; let the model interpret them →

  8. MinutePad ·Pulling the Phase 4 AI forward, but to do one thing only

    What I decided

    The design doc placed AI 12 to 24 months out, with Eisenhower classification and usage-pattern analysis. I judged Pro needed a reason to buy today, but matrix classification without usage data would be hollow. So AI moved into v1.0 with a narrow job, 'fit today's unscheduled tasks into the day', and every suggestion requires my confirmation.

    What AI did

    Claude built AIAssistView and suggestSchedule: pick up to five tasks, feed the model the current time, occupied slots and blocked slots, and demand HH:mm lines only; a line that fails to parse shows 'no suggestion' with a 'move to tomorrow' fallback.

    Outcome

    AI scheduling became the headline Pro feature (three free runs a day); the Eisenhower fields stay reserved and unused (DECISIONS D017, D020).

    Lesson Ship one AI feature that is accurate and checkable before chasing clever ones →

  9. MinutePad ·Widget check-offs must sync back to the app

    What I decided

    I asked for a home-screen widget where you can check tasks off directly, with the change always syncing back to the app, but I did not want the widget, a separate process, writing straight into the Core Data store.

    What AI did

    Claude used an AppIntent (ToggleTaskIntent) to append the tapped task ID to a pending queue in the App Group, with optimistic display on the widget; when the app returns to the foreground it drains the queue into Core Data and pushes the fresh list back to the widget.

    Outcome

    Widget and app talk only through a one-way push plus one queue in App Group UserDefaults, with no multi-process writes to the same store (DECISIONS D023).

    Lesson For cross-process sync, decide first who the single writer is →

  10. Taipei Zoo Course Assistant LINE Bot ·A 21-hour worker crash loop in production

    What I decided

    After the per-user message cap shipped, Cloud Run logs filled with gunicorn WORKER TIMEOUT and SIGKILL entries, starting exactly at that deploy. I handed Claude Code the logs and the timeline and asked for the link to the change, not just a bigger timeout.

    What AI did

    Claude Code found that rate_limiter, interest_store and conversation_memory each created their own Firestore client, up to three initialisations per message; it introduced a shared per-process singleton and widened the gunicorn timeout from 30 to 60 seconds as a safety net.

    Outcome

    The crash loop stopped. The reasoning lives in the firestore_client.py docstring so whoever comes next can see why.

    Lesson Find the root cause tied to the change first, then add the safety net →

  11. Taipei Zoo Course Assistant LINE Bot ·A home-made blind-testing skill: let an agent that does not know the answers ask

    What I decided

    Hand-written test cases inevitably favour questions I already know the bot can answer. I designed a Claude Code skill: I supply the persona (a relative pushing a wheelchair, a teacher planning a field trip), a sub-agent that has never seen the data folder does the asking, and it must write all its questions before asking any and classify only after the last answer.

    What AI did

    Claude Code ran two rounds, 35 questions in all, against production from an isolated sub-agent, pulled the full replies back from Cloud Logging and sorted them into four classes. The main agent logged each persona verbatim and, after the second round exposed a 'only the first log line was fetched' flaw, wrote the correct two-step retrieval back into the skill.

    Outcome

    Round one hit the permanent five-message cap at question six, raised to twenty the same day. Round two surfaced 'rest' being misread as a closure query and a group-ticket flow that would not let go, both fixed the next day. None of the three would have been caught by pre-designed test cases.

    Lesson Whoever asks must not know the answers; turn the test method itself into a reusable tool →

  12. Taipei Zoo Course Assistant LINE Bot ·A liveness check after the pipeline failed silently for three weeks

    What I decided

    The daily timetable pipeline was built to skip safely and retry tomorrow. After mid-September it skipped four runs in a row, and only in early October did I notice production had been answering from a stale timetable for three weeks. I asked for an alert, but not a 'no update in seven days' rule that would cry wolf every month.

    What AI did

    Claude Code wrote check_pipeline_liveness.py: the signal is whether the month in courses_meta.json lags the current month, with a grace window on days 1 to 7 to separate normal waiting from a real failure. It always exits 0, reporting rather than blocking, so it cannot become one more silent gate. It also added an authentication pre-check before each scheduled run.

    Outcome

    Every scheduled run now starts with the liveness check and writes its result as the first line of the report. Skipping is fine; skipping without anyone knowing is not.

    Lesson A fail-safe that skips safely but silently is only half built →

  13. MinutePad ·Drawing the AI's boundaries in CLAUDE.md

    What I decided

    A CBT-style 'small next step' suggestion and the seeds of habit analysis had crept into the code. I judged those belong to a different product, not to MinutePad's 'simple and effortless' positioning. So I wrote positioning, scope, the Pro boundary and an explicit 'not in this project' list into CLAUDE.md, turning it into a PRD for the AI.

    What AI did

    Claude Code reads CLAUDE.md at the start of every session and has stayed inside the scope since; the leftover suggest(for:) and AISuggestionCache remain as code with no UI entry point, pending removal.

    Outcome

    v1.0 scope held: AI only reschedules today's tasks, and CBT plus habit analysis were formally moved out (DECISIONS D025).

    Lesson In a requirements doc for an AI, 'what we do not build' does more work than 'what we build' →

  14. MinutePad ·The AI took a shortcut: a hard-coded API key

    What I decided

    The plan said AI scheduling would go through an Azure Functions relay to protect the key (D006). To get the feature running, Claude called Azure OpenAI directly from the app and put the key in Secrets.swift. My security check before the first git commit caught it; I laid out three options (ignore the file / inject via xcconfig / build the relay), chose xcconfig for now and put the relay on the pre-launch list.

    What AI did

    Claude moved the key into a git-ignored Secrets.xcconfig, injected it into Info.plist through an optional include in Base.xcconfig, and rewrote Secrets.swift to read it; the project still builds without the xcconfig and the AI feature reports 'not configured'. Both cases were verified with xcodebuild.

    Outcome

    No key in source or git history; the exposed key is scheduled for rotation. The relay is logged as known pre-launch debt (DECISIONS D019, D026).

    Lesson An AI will cross a security line to make something work; hold the line with a checkpoint in the process, not with its good intentions →

  15. MinutePad ·A tooling decision overturned by Xcode

    What I decided

    D012 chose XcodeGen to manage the project file. Then I added the Widget target and signing directly in Xcode, and project.yml fell behind for good. When tidying the docs I chose to accept reality: the pbxproj is owned by Xcode, and 'never run xcodegen generate' became a logged decision.

    What AI did

    While back-filling DECISIONS, Claude found project.yml had no Widget target and still carried placeholder bundle IDs, wrote it up as D015 and added the warning to the README setup steps.

    Outcome

    Docs now match how the project is really built, and a newcomer will not wipe the Widget target by running xcodegen (DECISIONS D015).

    Lesson A decision log must allow reversals, and the reversal itself must be logged →

  16. Chinese Homework Tracker ·Putting it under git: prove it is the live code first

    What I decided

    The source lived only in the Apps Script editor, and my Google account held several look-alike projects. I decided the source of truth is whatever the live URL is running, not whatever a project is named.

    What AI did

    Claude Code used clasp to compare each candidate project's deployment IDs against the live URL, then diffed the deployed version against HEAD file by file, and only then cloned it into the repo.

    Outcome

    src/ in the repo is byte-identical to the live version; the deployment URL is unchanged.

    Lesson Step one of version control is not the commit, it is proving what you commit is what runs →

  17. Chinese Homework Tracker ·Privacy review: not shown is not the same as not sent

    What I decided

    I asked for a check for hard-coded student data. Once it was explained that pressing F12 exposed every name, I decided to drop the name field and required the same deployment be updated so the URL would not change.

    What AI did

    Claude Code confirmed nothing was hard-coded but flagged that getStudentData's JSON carried the name column; per the rules it reported rather than silently changing it, and only after my decision removed one line and updated the same deployment with clasp deploy -i.

    Outcome

    The response no longer carries names; neither the screen nor the URL changed.

    Lesson Audit the payload, not the screen →