2026-03-11 09:29:07
What to do when Claude Code gets it almost right — and how one disciplined follow-up gets you the rest of the way there
Margaret is a senior software engineer. Timothy is her junior colleague. They work in a grand Victorian library in London — the kind of place where conversations are finished properly, and where almost right is understood to be a different thing entirely from right. Timothy has arrived today with something he is not sure what to do with.
He sat down without his usual preamble and turned the laptop toward her immediately. This, Margaret had learned, meant he wanted her to look before he explained. He was uncertain about something and did not yet have the words for it.
She looked.
On the screen was a function — a data export utility, clean and reasonably structured. She read it the way she always read code: slowly, without expression, beginning to end.
"Claude Code?" she said.
"Yes. Second attempt, actually. The first one was completely off. This one is..." He paused. "Close."
"Close," she repeated.
"It handles the main cases. The CSV export works. The date formatting is correct. But it doesn't handle empty datasets — it just throws an error instead of returning an empty file. And the column headers are hardcoded when they should come from the data schema."
Margaret looked at the code again. "And what did you do when you noticed this?"
Timothy hesitated. "I'm not sure whether to fix it myself, start over with a new prompt, or..." He trailed off.
"Or what?"
"Or tell Claude Code what's wrong and ask it to fix it."
Margaret set the laptop down between them. "That third option," she said. "That is what we are going to talk about today."
"Most developers," Margaret said, "when they receive output that is almost right, do one of three things. They accept it and patch it manually. They discard it and start over from the beginning. Or they send a vague correction and hope for the better."
"I've done all three," Timothy admitted.
"Most people have. None of them are wrong exactly — but none of them are the most effective approach either." She picked up her pen. "Patching manually makes sense for a trivial fix. Starting over makes sense if the output is fundamentally misconceived. But when the output is close — when the structure is sound and only specific behaviours are wrong — there is a better path."
"The follow-up."
"The follow-up," she confirmed. "Which is not simply sending another message. It is a skill. And like the first prompt, it requires thinking before typing."
"Think about what Claude Code knows at this point," Margaret said. "It has your original prompt. It has the solution it produced. It has no idea what you think of that solution — what worked, what didn't, what you expected that you didn't get."
"So I need to tell it."
"Precisely. But how you tell it matters enormously." She turned the notepad toward him. "A poor follow-up says: this doesn't work, try again. It gives no information. Claude Code will produce another attempt — possibly better, possibly worse, almost certainly not targeted at the actual problem."
Timothy was already nodding. He had sent that message before, in different words.
"A good follow-up," Margaret continued, "does three things. It acknowledges what worked. It identifies precisely what didn't. And it adds any context that was missing from the original prompt — because if the first output was wrong in a specific way, that wrongness often reveals a gap in what you originally provided."
She wrote three short phrases on the notepad:
What worked.
What didn't, and exactly how.
What I should have told you the first time.
"That third one," Timothy said. "That's the one people skip."
"Always," Margaret said. "Because it requires admitting that the original prompt was incomplete. Which it almost always is. The first output is not just a solution — it is a diagnostic. It shows you what Claude Code understood from what you gave it. When the output is wrong in a particular way, that wrongness is information about your prompt."
"Show me the error," Margaret said. "When the empty dataset is passed."
Timothy pulled up the output. The function threw a TypeError — it attempted to access the first row of the data to infer column structure, and when there was no first row, it failed.
"What does that tell you?" Margaret asked.
"That it assumed there would always be data."
"Yes. And why did it assume that?"
Timothy thought for a moment. "Because I didn't mention empty datasets in the prompt."
"Correct. The error is not a failure of Claude Code's ability. It is a reflection of your prompt's silence on edge cases." She looked at him steadily. "This is the diagnostic read. Before you write your follow-up, you ask: what did this output assume that I never specified? The answer to that question tells you exactly what to add."
He turned back to the laptop. "The column headers being hardcoded — that's the same thing. I said 'export to CSV' but I never said the headers should be dynamic."
"You assumed it would infer that from context. It did not." She allowed a small pause. "This is not a criticism of your original prompt. It is almost impossible to specify everything in advance. The follow-up exists precisely because the first exchange is a dialogue, not a transaction."
"Now write it," Margaret said. "The follow-up. Not yet — think first."
Timothy was quiet for a moment. Outside, London was doing what London does in the early evening — settling into itself, lights coming on in the buildings across the way.
"I need to acknowledge that the main export logic is correct," he said slowly. "So it doesn't throw that away and start over."
"Good."
"Then I need to describe the two specific problems. Not just name them — describe them. The empty dataset case should return an empty CSV with headers but no rows. The column headers should be derived from the schema object, not hardcoded."
"Better. And the third part?"
He thought. "I should tell it about the schema object. I don't think I mentioned it in the original prompt at all."
"You did not."
"So Claude Code invented the hardcoded headers because it had no idea the schema existed." He sat back. "The bug was in my prompt."
"The gap was in your prompt," Margaret said, not unkindly. "There is a difference. A bug implies carelessness. A gap implies incompleteness. The first prompt is rarely complete. That is why the follow-up exists."
Timothy wrote the follow-up carefully. He began by saying that the core export logic was working correctly and should be preserved. He then described each problem specifically — what was happening, what should happen instead, and under what conditions. Finally, he introduced the schema object he had omitted, describing its structure and how the headers should be derived from it.
He read it back aloud — the habit Margaret had taught him — and caught one imprecision. He corrected it before sending.
The response came back targeted and specific. The core logic was untouched. The empty dataset case now returned a header row with no data rows. The column headers were drawn cleanly from the schema.
"It didn't start over," Timothy said.
"Because you told it not to," Margaret said. "By describing what worked, you gave it permission to build on what already existed rather than replace it. That is not a small thing. A follow-up that begins with what is wrong invites a complete rewrite. A follow-up that begins with what is right invites a precise correction."
"I've been thinking of each prompt as a separate request," Timothy said. "Like placing an order."
"Most people do," Margaret said. "But that is not what it is. Claude Code holds the context of your conversation. It knows what you asked, what it produced, what you said in response. You are not placing a series of independent orders — you are having a conversation with a very capable colleague who happens to have no memory beyond the current session."
"So the follow-up is just... the next thing I would say in a conversation."
"Exactly that. If a colleague had built you this function and you found two problems with it, you would not hand it back and say this is wrong, do it again. You would say — the main logic is good, there are two things to address, and I should have mentioned this third thing earlier. That is a professional exchange. That is what a good follow-up sounds like."
Timothy looked at the corrected function on the screen. Clean, complete, handling every case he needed.
"How many follow-ups is too many?" he asked.
"That depends on what each one achieves," Margaret said. "If each follow-up produces meaningful progress toward the right answer, continue. If you find yourself sending the same correction and receiving the same gap in return — that is the signal to step back. Not to abandon the conversation, but to reconsider whether the context you have provided is sufficient." She paused. "Sometimes the right move is to start fresh with a prompt that incorporates everything you have learned from the previous exchange. The conversation was not wasted. It taught you what the prompt needed to contain."
The library was quieter now. The afternoon had become evening without either of them particularly noticing.
"There is one more thing," Margaret said. "When Claude Code produces something that is almost right — resist the urge to feel that almost right is good enough. It is not impatience or perfectionism to want the right answer. It is professional standards."
"I nearly accepted the hardcoded headers," Timothy said. "I thought — close enough, I can deal with it later."
"Later has a way of becoming never," Margaret said. "And a function with hardcoded headers will cause someone a problem at some point — possibly you, possibly someone who inherits your code, possibly a user. The follow-up costs five minutes. The fix costs considerably more when it arrives at the wrong time."
She picked up her tea.
"Claude Code is a patient correspondent. It does not tire of the conversation. It does not take corrections personally. It does not resent being asked to do better. Use that. The follow-up is not an admission that the first attempt failed — it is the natural continuation of a dialogue that was always going to take more than one exchange to get right."
He closed the laptop.
Outside, the lights of London had come fully on. Inside the library, a developer had just understood something that would change how he worked every day — that the first response is not the destination. It is the beginning of the conversation.
And knowing how to continue that conversation is a skill worth building.
Next episode: Margaret and Timothy turn to a different kind of challenge — reading code you didn't write. How to orient yourself in an unfamiliar codebase, and how Claude Code can help you find your footing without losing your judgment.
The Secret Life of Claude Code publishes every other day.
If this series is useful to you, share it with a developer who needs to hear it.
Aaron Rose is a software engineer and technology writer at tech-reader.blog. For explainer videos and podcasts, check out Tech-Reader YouTube channel.
2026-03-11 09:26:30
GPT-4, Claude 3.5, Gemini... Are you still keeping 10 AI tabs open in your browser, endlessly copy-pasting code between your IDE and chat UIs?
It's time to end the "Tab Hell."
Let me give you a slightly hot take: Just changing a system prompt to say "You are a reviewer" while hitting the exact same expensive model backend is not a true "Multi-Agent" system. That’s why I built multi-ai-cli. It’s a lightweight, Python-powered orchestrator designed to turn your terminal into a true multi-model battlefield.
Instead of just swapping prompts, we’ve completely separated the physical AI providers (Engines) from their logical roles (Agents).
Just configure the auto-generated ~/.multi-ai/config.ini file upon your first run like this:
# Example: Mapping Engines to Agents
ENGINE.openai_main = gpt-4o
ENGINE.local_coder = qwen2.5-coder:14b # Yes, Ollama works perfectly!
AGENT.gpt.architect = openai_main
AGENT.local.code = local_coder
Now you can route heavy architectural tasks to the smartest cloud models, and offload simple coding tasks to local models. Spin up Ollama’s qwen2.5-coder:14b locally, and you get a fully offline, API-key-free multi-AI experience! No vendor lock-in. You can freely switch to the optimal backend for each specific role.
When orchestrating multiple AIs, if you keep feeding a bloated conversation history to the APIs, you will face API bankruptcy in no time. To solve this, I split the memory into two layers:
@scrub)My most-used command. It flushes the messy conversation history instantly while keeping the Persona (system prompt) intact. It stops the AI from hallucinating on old context and saves your wallet.
% @sequence -e
[*] Editor prompt captured (182 chars, 13 lines):
--- Preview ---
[
@gpt Remember exactly this token: ZEBRA-9182
||
@gemini Remember exactly this token: ZEBRA-9182
]
-> @scrub gpt ->
[
@gpt What is the token?
||
@gemini What is the token?
]
--- End Preview ---
[*] Sequence Execution: 3 steps detected.
==================================================
[*] Executing Step 1/3 [PARALLEL: 2 tasks]...
Task 1: @gpt Remember exactly this token: ZEBRA-9182
Task 2: @gemini Remember exactly this token: ZEBRA-9182
--- GPT ---
Okay — I’ll remember this token exactly:
ZEBRA-9182
--- Gemini ---
I have memorized the token exactly: **ZEBRA-9182**.
Just let me know whenever you need me to recall it!
[✓] Step 1/3 completed (all parallel tasks done).
--------------------------------------------------
[*] Executing Step 2/3...
Command: @scrub gpt
[*] GPT memory scrubbed.
[✓] Step 2/3 completed successfully.
--------------------------------------------------
[*] Executing Step 3/3 [PARALLEL: 2 tasks]...
Task 1: @gpt What is the 'token?'
Task 2: @gemini What is the 'token?'
--- GPT ---
Which token do you mean?
If you mean:
- API token: I can’t see your secrets or account tokens.
- A “token” in text/LLMs: it’s a chunk of text a model processes, often a word or part of a word.
- Auth/session token in an app: it’s a credential used to prove identity.
Tell me the context and I’ll answer precisely.
--- Gemini ---
The token is ZEBRA-9182.
[✓] Step 3/3 completed (all parallel tasks done).
==================================================
[✓] Sequence Execution complete. All 3 steps succeeded.
%
-r / -w)Save the AI's output to local files (-w), and feed them into different models later (-r). The real magic here is State Recovery. If an automated pipeline fails halfway through, you don't have to start over. You just read the last saved file and design a new flow to recover from that exact point.
% @sequence -e
[*] Editor prompt captured (225 chars, 7 lines):
--- Preview ---
@sh "echo '<p>Hello World</p>'" -w raw.html
->
@gpt "Extract the text from this HTML" -r raw.html -w text.txt
->
@claude "Translate this text into Japanese" -r text.txt
->
@gemini "Translate this text into French" -r text.txt
--- End Preview ---
[*] Sequence Execution: 4 steps detected.
==================================================
[*] Executing Step 1/4...
Command: @sh 'echo '"'"'<p>Hello World</p>'"'"'' -w raw.html
[*] @sh: Executing: echo '<p>Hello World</p>'
[✓] @sh: SUCCESS (exit code: 0, 12.0ms)
--- stdout ---
<p>Hello World</p>
--- end stdout ---
[*] @sh: Artifact saved to 'raw.html' (format: text).
[✓] Step 1/4 completed successfully.
--------------------------------------------------
[*] Executing Step 2/4...
Command: @gpt 'Extract the text from this HTML' -r raw.html -w text.txt
[*] Result saved to 'text.txt' (mode: raw).
[✓] Step 2/4 completed successfully.
--------------------------------------------------
[*] Executing Step 3/4...
Command: @claude 'Translate this text into Japanese' -r text.txt
--- Claude ---
--- [File: text.txt] ---
こんにちは世界
--- [End of File: text.txt] ---
[✓] Step 3/4 completed successfully.
--------------------------------------------------
[*] Executing Step 4/4...
Command: @gemini 'Translate this text into French' -r text.txt
--- Gemini ---
Bonjour le monde
[✓] Step 4/4 completed successfully.
==================================================
[✓] Sequence Execution complete. All 4 steps succeeded.
%
@sequence)
Write human-AI hybrid workflows like code. Use -> for sequential steps, and [ A || B ] for parallel execution.
# Fetch code from GitHub, run parallel reviews, and merge the results
@sequence
-> @github.file --repo "myproj/repo" --path "app.py" -w code.md
-> [ @claude.review "Find bugs" -r code.md -w claude_review.md
|| @gemini.plan "Optimization ideas" -r code.md -w gemini_opt.md
|| @gpt.code "Add test cases" -r code.md -w tests.py ]
-> @gpt "Merge the 3 reviews above and create the final version" \
-r claude_review.md -r gemini_opt.md -r tests.py -w final.py
(💡 Pro Tip: Open another terminal and run tail -f logs/chat.log. You get a real-time HUD monitoring all AI conversations as they happen! Debugging is an absolute breeze.)
@sh)
# Example 1: Preprocess text before passing it to an AI agent
@sh "cat raw.html | sed 's/<[^>]*>//g'" -w text.txt
# Example 2: Inspect local project files or command output
@sh "ls -la src"
@sh "git diff --stat"
# Example 3: Run local scripts or test suites seamlessly within your workflow
@sh "python scripts/build_index.py"
@sh "pytest tests/"
This is the ultimate terminal advantage that browsers can't touch. Pipe the output of any CLI tool directly into the AI.
# Example 4: Run linters and let Claude fix the errors
@sh "flake8 app.py" -w lint.md
->
@claude "Fix all these lint errors" -r lint.md -r app.py -w fixed.py
Even with all this power, our adapters (like GitHub and Figma) are strictly Read-Only. This is a deliberate safety-by-design choice.
We all dread the nightmare of an autonomous agent going rogue and git push-ing broken code while you're getting coffee.
Analysis and generation are the AI's job. The final Write (commit) is your responsibility.
By keeping the human in the loop, you maintain absolute control over your codebase. This makes it a tool you can actually trust in a real-world workflow.
multi-ai-cli is currently at v0.13.0. To avoid registry bloat and keep things blazingly fast, you have two hacker-friendly ways to get started:
git clone [email protected]:ashiras/multi-ai-cli.git
cd multi-ai-cli
uv sync # Install dependencies
uv run multi-ai --version # Verify the installation
# Run it like this from now on!
# uv run multi-ai "@gpt Hello world"
Don't want to mess with environments at all? You can download the latest pre-built binary directly from our GitHub Releases (macOS / Linux / Windows supported). It's a zero-dependency single file!
# Example for macOS / Linux
curl -L -o multi-ai https://github.com/ashiras/multi-ai-cli/releases/download/v0.13.0/multi-ai
chmod +x multi-ai
sudo mv multi-ai /usr/local/bin/ # Or to ~/bin/ etc.
multi-ai --version
(Note: Please check the Releases page for the exact URL for your specific OS!)
I can never go back to juggling 10 AI browser tabs. Upgrade your terminal into the ultimate multi-AI battlefield today!
2026-03-11 09:20:18
21.1%. That is the annual retention rate for AI-powered subscription apps right now. Non-AI apps hold at 30.7%. You built the smarter product. You are losing subscribers nearly 30% faster. And the revenue you are collecting on the way in does not offset what you are bleeding on the way out.
I have read every line of RevenueCat's 2026 State of Subscription Apps report, built from over 115,000 apps, $16 billion in processed revenue, and more than a billion transactions. The churn pattern inside AI apps is not subtle. It is structural. And it will not be fixed by adding another feature.
Why AI App Annual Retention Is 21.1% While Non-AI Apps Hold at 30.7%
The data is from RevenueCat's 2026 State of Subscription Apps report, released in March 2026. It covers iOS, Android, and web subscription apps across every major category.
AI-powered apps churn annual subscribers 30% faster than non-AI apps at the median. Monthly, AI apps retain 6.1% of subscribers versus 9.5% for non-AI. The only metric where AI apps outperform is weekly retention at 2.5% versus 1.7%, and weekly subscriptions are not the plan type most AI apps sell.
Most developers read this and conclude the product needs work. Better outputs. Faster inference. Smarter prompts. They go build.
That is the wrong diagnosis.
The gap is not a product quality problem. It is a perceived value problem that compounds month over month. AI apps spike on novelty. Users convert because the demo is sharp. Then the 40th AI-generated output lands, and they cannot articulate why they are still paying $14.99 a month for something that feels like every other app on the store.
The systemic reason: AI apps are solving problems users did not know they had. That creates curiosity-driven subscriptions, not commitment-driven ones. Curiosity does not renew.
The fix: Anchor your onboarding to outcomes the user already tracks. Not features. Outcomes with numbers attached. "You saved 4.3 hours this week" holds a subscription. "Here is your AI summary," does not.
The Day 0 Problem: 55% of 3-Day Trial Cancellations Happen Before Day 1
This is the number most developers skip over in the RevenueCat report. 55% of all 3-day trial cancellations happen on Day 0. In the same session, the user downloaded the app. Before they have seen anything past the onboarding screen.
Most teams optimize for the paywall. The copy. The price point. The trial length. They A/B test the button color. Meanwhile, more than half of their potential subscribers are leaving during the first session before the trial even starts.
What they think is a pricing problem is actually a first-session experience problem. The user opens the app, hits friction, does not understand the value fast enough, and cancels before they have given the product a real chance.
For AI apps, this is worse. The aha moment in an AI app usually requires the user to input context. To set up a profile. To run a query and wait for a result. That setup cost kills day zero conversions because the payoff is deferred.
The fix: Front-load the output. Show the user what your AI can do before you ask them to do anything. Give them a pre-loaded example, a demo result, a preview of the insight. Make the value visible in under 60 seconds. Then ask them to set up their account.
How Android's Billing Failure Rate Is Destroying AI App Retention on Google Play
Nearly one-third of all subscription cancellations on Google Play are involuntary billing failures. On the App Store, that rate is 14%. Android developers are losing subscribers at more than twice the rate of iOS due to a problem that has nothing to do with their product.
For AI apps that skew toward cross-platform audiences, this is a silent revenue leak. The user did not decide to leave. Their payment failed. The subscription lapsed. They never came back. That outcome shows up in your retention data as churn, but it is actually a billing infrastructure problem.
The RevenueCat report frames this directly: for Android developers, fixing billing failure is the highest-leverage retention move available right now. Not a new feature. Not better prompts. Billing recovery.
The fix: Implement a billing grace period with a re-engagement sequence. RevenueCat's platform has dunning management built in. If you are running Android subscriptions and you are not using them, you are leaving recoverable revenue on the table every single month.
Why Vibe Coded AI Apps Are Accelerating the Churn Problem Across iOS and Android
14,700 new subscription apps launched in January 2026 alone. A growing share of those are AI apps built with AI-assisted development tools, shipped in days, monetized through RevenueCat in hours. The stores are flooded.
iOS now accounts for 77% of all new subscription app launches, up from 67% in 2023. The steepest acceleration began in early 2025, when AI-assisted development tools became mainstream. The result is a market where differentiation at the product level is nearly impossible because the underlying models are commodities. Every AI writing app is drawing from the same model family. Every AI health coach produces roughly similar outputs. The user cannot tell the difference. So they chase the new thing.
This is the SaaSpocalypse playing out in real time inside the App Store. More supply, same demand, lower switching cost. The user who cancels your AI app in month three is not going back to doing things manually. They are subscribing to the next AI app that showed up in their feed.
The fix: Build one layer of retention that the model cannot replicate. Community. Streak. Accountability check-in. A persona with actual memory of what the user told it six weeks ago. Something that makes switching cost something.
The Hard Paywall vs. Freemium Trap That Is Costing AI Apps Their Best Users
Hard paywalls convert 5x better than freemium. 10.7% conversion rate versus 2.1%. That number from the RevenueCat 2026 report looks like a clear signal to put up the gate and collect.
Here is what that number does not show: after 12 months, hard paywall retention and freemium retention are nearly identical. The conversion advantage disappears over time. The users who converted fast under a hard paywall churn at the same rate as everyone else.
For AI apps specifically, the hard paywall creates a structural problem. The user commits before the AI has earned the commitment. They pay upfront for a product they have not yet experienced. When the novelty fades, usually around month two, they have no established habit, no visible progress, and no reason strong enough to justify renewal.
The fix: For AI apps, extend the trial or use a freemium gate that unlocks after the user completes a meaningful action. Not after 7 days. After the user has experienced a real outcome. Let the AI prove it can do the thing you promised. Then ask for the subscription.
What Hybrid Monetization Actually Fixes for AI App Developers in 2026
35% of apps now layer subscriptions with consumables or lifetime purchases. For AI apps with real variable costs tied to model inference, this is no longer optional. It is structural.
A flat subscription that covers unlimited queries works for 1,000 users. At 100,000 users with power users running 500 queries a month, the math breaks. Margins compress. The product gets throttled, or the developer absorbs the cost. The power user notices the degradation. They churn angrily. The review drops. The ranking follows.
Hybrid monetization solves two problems at once. Credit-based top-ups layered on a base subscription align pricing with real cost. They also give your highest-value users a reason to stay. Power users self-select into higher spend instead of being subsidized by everyone else, and they generate the infrastructure costs they incur.
The fix: Identify your top 10% of users by usage volume. Build a credit model that lets them go beyond the base plan. They fund their own usage. They feel seen. They do not churn. This is not a monetization experiment. It is what the data says the market is moving toward in 2026.
The One Retention Fix AI App Developers Consistently Overlook
Non-AI apps are better at making value visible. A fitness app shows a streak. A budgeting app shows money saved. A language app shows words learned this week. The user can point to a number and justify the subscription cost in under five seconds.
AI apps show outputs. Outputs are invisible values. The user cannot tell if the summary was good or barely adequate. They cannot see the time they saved because the alternative was never clearly quantified. By month 11, they cannot remember why they started subscribing.
The fix: Build a value dashboard. Not a features dashboard. A dashboard that shows the user what the AI has done for them in measurable terms since day one. Time saved. Decisions supported. Tasks completed. Documents processed. Give them a number. That number is what they are actually paying for. That number is what keeps them from canceling.
The apps that win 12-month retention in 2026 are not the ones with the best model. They are the ones who made the value of that model impossible to ignore every time the user opened the app.
Most AI apps let that moment slip every single day.
Next, I am looking at why Android is losing ground to iOS in new subscription launches despite growing in absolute volume. That is where the platform monetization gap for AI apps actually lives.
2026-03-11 09:20:03
A few weeks after the launch of iOS 13, a client asked us to change the existing Photo Markup Kit in our app to Apple’s inbuilt PencilKit. As PencilKit might be new to you, we’ve created a guide that introduces you to PencilKit. For reference, PencilKit looks the same as the below image:
PencilKit allows developers to easily integrate the drawing canvas view and toolkit in their applications. It makes easy to incorporate hand-drawn content into iOS apps quickly and easily. PencilKit also creates a Canvas\ to draw over it with various given tools.
1. Eraser Tool: Using this tool, users can delete a specific object or some part of the object drawn on the Canvas.
2. Inking Tool: This includes pen, marker, and pencil for different kinds of sketches. Users can select different colors available for the inking tool.
3. Lasso Tool: This tool is a selection tool for selecting a drawing area. Additionally, it gives a context menu that allows cut, copy, paste and duplicate the selected drawing.
Our goal was to use PencilKit to draw markups over a selected image in our app and save the image with the drawn markups.
We read many tutorials and watched several videos to understand the working implementation of PencilKit. All the tutorials we went through were explaining the use of PencilKit to draw over a blank canvas and save the drawing. But we did not find anything related to draw over an existing image using PencilKit and save the image with drawn markups.
We went through the implementation details of PencilKit given in Apple Developer Documentation. We also looked into its classes, sub-classes, and properties to find a way to insert an existing image in PencilKit’s CanvasView\ and get the output as a marked-up image.
After this research we concluded the following:
We discussed in a team and decided that we will try to build a custom solution for Image Markup using PencilKit.
After discussion and research we finalized two approaches for implementation:
On comparing both the approaches we found that the second approach is more suitable and feasible.
To start with the implementation we need to import PencilKit into our project #import PencilKit. We need to create a CanvasView to draw with PencilKit and an ImageView. So we created a ViewController with 3 properties, an ImageView, a CanvasView, and an Image.
@IBOutlet weak var imgView: UIImageView!
var canvasView: PKCanvasView!
var imgForMarkup: UIImage?
Now we will initialize the CanvasView and add it as subview. We will do it in ViewDidAppear Method :
self.canvasView = PKCanvasView.init(frame: self.imgView.frame) self.canvasView.isOpaque = false self.view.addSubview(self.canvasView)
Per our requirements, we have to place a transparent CanvasView over an ImageView. Here we have to consider that ImageView frame will cover the full screen of the device but different Images showing in the ImageView will have different height and width ratios. That means the image will appear over some part of the ImageView and the rest of the ImageView rect will remain vacant. We need to draw markups, only over the Image, and not over the vacant spaces outside the Image rect. So we need to resize our CanvasView to our image’s visible rects every time we load the controller and in case if device’s orientation is changed. For doing this we will set the frame of our CanvasView respective of the image.
self.canvasView.frame = self.setSize()
func setSize() -> CGRect { let containerRatio = self.imgView.frame.size.height/self.imgView.frame.size.width let imageRatio = self.imgForMarkup!.size.height/self.imgForMarkup!.size.width if containerRatio > imageRatio { return self.getHeight() }else { return self.getWidth() } }
func getHeight() -> CGRect { let containerView = self.imgView! let image = self.imgForMarkup! let ratio = containerView.frame.size.width / image.size.width let newHeight = ratio * image.size.height let size = CGSize(width: containerView.frame.width, height: newHeight) var yPosition = (containerView.frame.size.height - newHeight) / 2 yPosition = (yPosition < 0 ? 0 : yPosition) + containerView.frame.origin.y let origin = CGPoint.init(x: 0, y: yPosition) return CGRect.init(origin: origin, size: size) }
func getWidth() -> CGRect { let containerView = self.imgView! let image = self.imgForMarkup! let ratio = containerView.frame.size.height / image.size.height let newWidth = ratio * image.size.width let size = CGSize(width: newWidth, height: containerView.frame.height) let xPosition = ((containerView.frame.size.width - newWidth) / 2) + containerView.frame.origin.x let yPosition = containerView.frame.origin.y let origin = CGPoint.init(x: xPosition, y: yPosition) return CGRect.init(origin: origin, size: size) }
After setting CanvasView frame we will add PencilKit’s tool picker to our CanvasView
self.canvasView?.drawing = PKDrawing()if let window = self.view.window, let toolPicker = PKToolPicker.shared(for: window) {
toolPicker.setVisible(true, forFirstResponder: self.canvasView)toolPicker.addObserver(self.canvasView)self.updateLayout(for: toolPicker)self.canvasView.becomeFirstResponder()}
We can also add a clear button to clear all the markups drawn over the CanvasView. Then, we can set IBAction of the button as,
@IBAction func onClear(_ sender : UIButton) { canvasView.drawing = PKDrawing() }
With this implementation, we can draw Markups, only with Apple Pencil. We need to set one more property of CanvasView to make it work with our Finger, considering not every user has Apple Pencil.
self.canvasView.allowsFingerDrawing = true
So now we can draw over the image using our finger as well. Now after all the above steps, our Image Markup ViewController will work like :
After editing we have to save the image with the drawn Markups. For this, we will get an output image of our drawing using PencilKit. The output image we get from PencilKit will be in PNG format with transparency. After that, we will draw the PNG image as an overlay on our selected image. For this we have a function :
@IBAction func saveDrawing(_ sender : UIButton) {var drawing = self.canvasView.drawing.image(from: self.canvasView.bounds, scale: 0)if let markedupImage = self.saveImage(drawing: drawing){// Save the image or do whatever with the Marked up Image......}...self.navigationController?.popViewController(animated: true)}func saveImage(drawing : UIImage) -> UIImage? {let bottomImage = self.imgForMarkup!let newImage = autoreleasepool { () -> UIImage inUIGraphicsBeginImageContextWithOptions(self.canvasView!.frame.size, false, 0.0)bottomImage.draw(in: CGRect(origin: CGPoint.zero, size: self.canvasView!.frame.size))drawing.draw(in: CGRect(origin: CGPoint.zero, size: self.canvasView!.frame.size))let createdImage = UIGraphicsGetImageFromCurrentImageContext()UIGraphicsEndImageContext() return createdImage!} return newImage}
From saveImage(drawing:) method we will get a Markedup Image that we can save in the device or share it as per our requirement.
However, with so many things coming out of the box, you don’t have a lot of flexibility for customization. For instance, several things I’ve encountered are:
PencilKit is a new framework, which provides a lot of drawing functionality out-of-the-box and it’s easy to get started and to integrate it in an app. PencilKit works seamlessly with Apple Pencil and it has high precision and low latency. With PencilKit, instead of re-inventing the wheel, you can focus on your app’s unique features.
There is a lot to love about it, so we hope Apple will overcome limitations in PencilKit in next iOS release.
Originally published at particle41.com
2026-03-11 09:16:18
You give it a problem. It builds a cross-functional team, assigns missions, and executes everything in dependency-based waves — using real parallel subagents.
That is what Assemble does. Here is exactly what using it looks like.
AI tasks are still mostly serial. Research, then spec, then plan. Each step waits for the last. But that is not how teams work — teams run in parallel, with dependencies managed by a coordinator.
Assemble brings that model to Claude Code. You describe the problem. A Project Manager agent organizes teams, assigns missions, and runs waves of parallel subagents until everything is done.
Let us build a CLI tool that analyzes git history and outputs a developer personality report.
You run /assemble. The PM asks 4 questions, one at a time:
PM: What are we building or solving?
> Build a CLI tool that analyzes git history and outputs a developer
personality report. Should work on any public GitHub repo.
PM: What constraints matter? (timeline, tech stack, budget, team preferences)
> Python only. No paid APIs. Should run in under 5 seconds on a repo
with 1000 commits.
PM: What does done look like? What would make this a success?
> A pip-installable CLI: `gitdna analyze owner/repo`
Outputs a 1-page markdown report with archetype, patterns, and stats.
PM: Any teams or roles you want included or excluded?
> Include Research and Engineering. Skip Design and Infra.
Add QA — I want a test plan.
After the 4 answers, the PM confirms what it heard:
Got it. Here is what I am working from:
· Goal: pip-installable CLI that analyzes git history → personality report
· Constraints: Python only, no paid APIs, <5s on 1000 commits
· Done: `gitdna analyze owner/repo` produces a markdown archetype report
Teams: Research, Engineering, QA
Moving to Phase 2.
The PM builds the project board and shows you everything before touching a single file:
ASSEMBLE — Project Board
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Mission: pip-installable CLI that analyzes git history → personality report
Constraints: Python only, no paid APIs, <5s on 1000 commits
Done looks like: `gitdna analyze owner/repo` outputs a markdown archetype report
[WAVE 1] — Starting immediately
┌ Research Team
│ Mission: Evaluate git parsing libraries and identify commit patterns
│ that map to distinct developer archetypes
│ Tasks:
│ · Survey gitpython, pygit2, dulwich — compare API ergonomics and speed
│ · Identify 5–8 measurable commit signals (message tone, time of day,
│ file churn, commit size, etc.)
│ · Map signals to 4–6 personality archetypes with concrete examples
│ · Document recommended library and signal extraction approach
│ Output: docs/research-notes.md
│ Status: ⏳ Pending approval
[WAVE 2] — Unlocks after Wave 1 approval
┌ Engineering Team
│ Mission: Build the CLI and report generator based on research output
│ Tasks:
│ · Scaffold pip-installable package with pyproject.toml and Click CLI
│ · Implement git history fetcher using recommended library from research
│ · Build signal extractor — reads commits, outputs feature dict
│ · Build archetype classifier — maps features to personality type
│ · Build markdown report generator — 1-page output with stats and narrative
│ Output: docs/implementation-plan.md + working code in src/
│ Depends on: Research
│ Status: 🔒 Locked
┌ QA Team
│ Mission: Design test strategy and acceptance criteria for the CLI
│ Tasks:
│ · Define test fixtures — sample git histories for each archetype
│ · Write unit tests for signal extractor and classifier
│ · Write integration test: clone a real repo, run CLI, validate output shape
│ · Define performance acceptance criteria: <5s on 1000 commits
│ Output: docs/qa-checklist.md
│ Depends on: Research, Engineering
│ Status: 🔒 Locked
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Ready to execute. Approve? (yes / adjust [instruction])
You see every wave, every team, every task — before anything runs. You can say adjust skip the signal extraction doc or adjust add a 5th task to Research and the board updates. Or just:
> yes
Wave 1 kicks off. The PM spawns the Research Team agent and shows you it is running:
Wave 1 — executing...
→ Research Team agent spawned
The Research agent does real work: reads documentation, evaluates libraries, writes a structured findings file to docs/research-notes.md. When it finishes, it sends a structured report back to the PM:
status: complete
confidence: high
completed_tasks:
- Evaluated gitpython, pygit2, dulwich — gitpython recommended
- Identified 6 commit signals: message length, time-of-day spread,
file type mix, commit frequency, churn rate, refactor ratio
- Mapped to 5 archetypes: The Architect, The Firefighter, The Craftsman,
The Experimenter, The Finisher
artifacts_written:
- docs/research-notes.md
key_findings:
- gitpython is 3x slower than pygit2 on large repos — recommend pygit2
- Time-of-day spread is the strongest single predictor of archetype
- Commit message length alone is a weak signal; combine with churn rate
next_step: Engineering can use docs/research-notes.md as input
The PM updates the board and shows you the Wave 1 checkpoint:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ Wave 1 complete.
Research Team ✅ docs/research-notes.md written
Key findings:
· Research: pygit2 recommended over gitpython (3x faster on large repos)
· Research: time-of-day spread is the strongest archetype predictor
· Research: 5 archetypes defined — Architect, Firefighter, Craftsman,
Experimenter, Finisher
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Continue to Wave 2? (continue / adjust [instruction] / stop)
You read the findings. You decide.
> continue
Wave 2 kicks off. Engineering and QA run in parallel — both get spawned at the same time. Engineering gets docs/research-notes.md as input. QA gets both research and engineering output.
Wave 2 — executing...
→ Engineering Team agent spawned
→ QA Team agent spawned
Both run simultaneously. Engineering writes the implementation plan and scaffolds the code. QA writes the test strategy based on the research and engineering outputs.
When both return:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ Wave 2 complete.
Engineering Team ✅ docs/implementation-plan.md written
✅ src/gitdna/ scaffolded
QA Team ✅ docs/qa-checklist.md written
Key findings:
· Engineering: pygit2 + Click stack confirmed, CLI entry point working
· Engineering: archetype classifier uses weighted signal scoring
· QA: 12 unit tests defined, 3 integration scenarios, perf target <5s
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
All waves complete. Moving to Phase 4.
The PM writes docs/executive-summary.md and lists every artifact:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ASSEMBLE — Complete
Artifacts written:
docs/research-notes.md
docs/implementation-plan.md
docs/qa-checklist.md
docs/executive-summary.md
src/gitdna/__init__.py
src/gitdna/cli.py
src/gitdna/parser.py
src/gitdna/classifier.py
src/gitdna/reporter.py
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
You now have: a research-backed library recommendation, a full implementation plan, working scaffolded code, and a test strategy — all from a single /assemble run.
While any of this is running, you can ask:
> What is QA doing?
PM: QA Team — Wave 2, currently in progress.
Mission: Design test strategy and acceptance criteria.
Writing: docs/qa-checklist.md
Status: running (no blockers reported)
> Show project status
PM: [renders full board with current statuses]
No new agents spawned. The PM answers from in-memory board state.
If a team fails, the PM does not give up or fill in the gap itself. It retries once with adjusted scope:
⚠️ Infra Team — blocked: no cloud credentials available in environment
Retrying with adjusted scope (local mock environment)...
✅ Infra Team — partial complete. docs/infra-plan.md written (mock config only)
If the retry also fails:
🔴 Infra Team — blocked after retry: mock environment insufficient for VPC config
Downstream teams that depend on this: Engineering
Hold (wait for you to resolve) or continue (proceed without Infra output)?
(hold / continue)
You decide. The PM waits.
Four files:
SKILL.md ← PM prompt registered with Claude Code plugin system
team-agent.md ← Contract template every spawned agent receives
team-library.md ← 8 default teams: missions, roles, output artifacts
status-schema.md ← Structured report format (status, confidence, findings)
The PM never touches project artifacts directly. It organizes, delegates, and reports. Every team agent writes a real file — no hallucinated summaries.
# Copy support files
mkdir -p ~/.claude/skills/assemble
git clone https://github.com/LakshmiSravyaVedantham/assemble.git /tmp/assemble
cp /tmp/assemble/team-agent.md ~/.claude/skills/assemble/
cp /tmp/assemble/team-library.md ~/.claude/skills/assemble/
cp /tmp/assemble/status-schema.md ~/.claude/skills/assemble/
# Register with Claude Code (requires superpowers plugin)
SKILLS_DIR=~/.claude/plugins/cache/claude-plugins-official/superpowers/5.0.0/skills
mkdir -p "$SKILLS_DIR/assemble"
cp /tmp/assemble/SKILL.md "$SKILLS_DIR/assemble/SKILL.md"
Then open Claude Code and run /assemble.
2026-03-11 09:12:26
It happened in the shower, or on a walk, or staring at a whiteboard with a dried-out marker in your hand. The solution arrived not as words, not as syntax, but as a shape — data flowing from here to there, branches splitting, paths converging, the whole thing hanging in your mind like a mobile. You can see it. You can trace any path through it.
Then you open your editor and start dismantling it.