<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[nullabletype.dev]]></title><description><![CDATA[Technical lead with 20+ years experience in software development. I architect solutions, write code, deploy systems, build awesome teams & deliver projects. Spe]]></description><link>https://nullabletype.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 02 Nov 2026 06:48:19 GMT</lastBuildDate><atom:link href="https://nullabletype.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building an agentic workflow, part 2: testing what done means]]></title><description><![CDATA[Working through the dot-finance prototypes, including a misleading interruption test, interface corrections, CI storage failures, and a supervised…]]></description><link>https://nullabletype.hashnode.dev/building-an-agentic-workflow-part-2-testing-what-done-means</link><guid isPermaLink="true">https://nullabletype.hashnode.dev/building-an-agentic-workflow-part-2-testing-what-done-means</guid><category><![CDATA[AI]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[loop]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[Steven Davies]]></dc:creator><pubDate>Tue, 27 Oct 2026 00:01:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/617c75a975401e57d6a87f3c/2efc647e-8ef9-46cf-aee2-83bffcf053b6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>At the end of part 1, I had a specification, prototype tickets, and rules for how an agent should work through them. There was no application or controller yet. The next step was to try those rules against something that could actually fail.</p>
<p>The dot-finance prototypes gave me a desktop application to use, encrypted storage and backup experiments, and a small controller to manage the agent's work and stop it when it reached the agreed limits. They also gave me a test that passed for the wrong reason, an interface that needed several rounds of correction, and a build pipeline that ran out of artifact storage.</p>
<p>By the end of this stage, I had a good base for production development to start. Getting there involved looking quite closely at what the passing checks established, and what they hadn't tested at all.</p>
<h2>The interruption test hadn't interrupted anything</h2>
<p>The persistence prototype could save and reopen synthetic data, restore backups, and reject the wrong-password and damaged-file cases covered by the tests. Independent agent review then found a gap in the interruption coverage.</p>
<p>Asking an operation to cancel gives it an opportunity to clean up. Killing the process during a backup doesn't. I needed to know what happened in that second condition.</p>
<p>The agent added a harness that launched a child process and attempted to kill it at a controlled checkpoint. Initially, the harness accepted any unsuccessful exit as evidence that the interruption had happened. Unfortunately, the process-kill call itself threw an exception.</p>
<p>The test appeared to pass because something had failed. It hadn't reached the failure I wanted to test.</p>
<p>The missing partial backup file helped expose the mistake. Fixing the kill call dealt with that immediate problem, but a further review required an explicit marker showing that the process had reached the intended checkpoint. The checks then had to establish that recovery worked and the previous completed backup was unchanged.</p>
<p>This was a useful example of why independent review in the loop is so important. The code and its test could agree with each other while missing the requirement. A reviewer asking what had actually happened exposed a much weaker check than the result suggested. The same reason I'd ask a colleague for a second pair of eyes when I've been deep in the weeds.</p>
<p>The corrected test still had a specific scope. It demonstrated recovery from interruption at a controlled checkpoint, not at every possible instant during a write. Separate agent review wasn't a specialist security audit either. That gave me more confidence in the backup handling, but there was still plenty it hadn't tested.</p>
<h2>The move from mock-up to actual app was rough</h2>
<p>I'd chosen a specific UI direction from the mockups, but the first workbench implementation had strayed significantly from it. It'll be interesting to investigate this more in the future, as I think this may be something more common to less used frameworks like Avalonia over say HTML or Typescript. Either way, this needed to be addressed.</p>
<img src="https://files.nullabletype.dev/posts/dot-finance-part-2-first-workbench.png" alt="The first workbench implementation, with a permanent password field in the header, small text and transaction content intruding into neighbouring columns." style="display:block;margin:0 auto" />

<p><em>The first workbench implementation I sent back with feedback. The data is fictional.</em></p>
<p>The feedback was ordinary desktop application feedback. Text was too small, icons were ambiguous or missing, columns didn't line up properly, and the scrollbar obscured the amount column. The password field had become a permanent fixture in the header, when it belonged with saving and reopening the data.</p>
<p>I also needed category totals visible while reviewing transactions. A final confirmation screen wouldn't do the same job. If I'm changing an allocation, I need to see its effect alongside the transaction I'm changing. That became an explicit specification and acceptance requirement.</p>
<p>The corrections brought in recognisable outline icons, larger text, clearer column boundaries, and password prompts attached to the operations that needed them.</p>
<img src="https://files.nullabletype.dev/posts/dot-finance-part-2-prototype-review.png" alt="The later prototype showing a populated transaction ledger, selected transaction editor and the visible upper portion of running totals." style="display:block;margin:0 auto" />

<p><em>The later prototype after feedback and correction. The window size and fictional examples differ from the earlier capture.</em></p>
<p>There were several rounds of this. At smaller window sizes, the transaction list could paint over the right-hand panel. Contained horizontal scrolling and truncation within columns addressed that, but I later caught overlapping balance labels and overflowing rule controls as well.</p>
<img src="https://files.nullabletype.dev/posts/dot-finance-part-2-balance-overlap.png" alt="A compact balance panel with labels colliding with the input fields." style="display:block;margin:0 auto" />

<p><em>One of the compact-layout problems I caught while trying the prototype.</em></p>
<p>A successful build and a screenshot capture could coexist with something I could immediately see was awkward. For the repeatable problems, the agent added compact-window captures and geometry assertions for the ledger, inspector, balance panel, and rule controls. Those gave later changes something concrete to fail against. Deciding whether the result was clear and comfortable to use still needed me to use it.</p>
<p>Review also found behavioural gaps. Unresolved items weren't always blocking acknowledgement, changed transactions could be silently skipped on re-import, and foreign-currency rows didn't require an actual GBP amount. Those went into a separate repair issue before the production decision.</p>
<p>Accepting the interface direction hadn't meant accepting every behaviour in the prototype. Keeping those decisions separate let me retain the layout and address the omissions.</p>
<p>These corrections were also giving me clearer UI requirements and checks to carry into production development.</p>
<h2>Corrections needed to survive the next session</h2>
<p>Some other feedback also changed the rules for subsequent work.</p>
<p>During the early hosted builds, I caught a GitHub Actions runtime deprecation warning. The Node version selected for the project was separate from the runtime used internally by an action. Updating the project's version hadn't addressed the warning.</p>
<p>That led to an explicit repository requirement for supported packages, actions, and images. The storage work also moved from the original SQLCipher bundle, recorded as deprecated during the project, to SQLite3MC. An authenticated snapshot experiment was feasible, but would have introduced a bespoke format and whole-file writes where an embedded database already suited the work.</p>
<p>Live totals went into the specification. Compact-layout defects became regression checks. Supported tooling became a repository rule. These were decisions a fresh session could find without me repeating the conversation.</p>
<p>But the budget rule from part 1 hadn't been demonstrated in the early persistence work. Correction counts were recorded after the fact, and CI repairs continued beyond the recorded count without a clear budget-renewal checkpoint. Writing down three attempts or 90 minutes hadn't made the limit enforce itself. The controller still needed its own experiment.</p>
<h2>Retaining evidence filled the storage allowance</h2>
<p>GitHub Actions artifact storage was a real pain, and a time sink I hadn't wanted from this experiment. It also showed how AI can still make some baffling decisions, like uploading hundreds of megabytes on every run without checking whether any of it needed keeping.</p>
<p>The workflow uploaded application output and validation evidence from each platform. When I asked what it was keeping, the investigation estimated roughly 680 MB of compressed output across three large uploads per run. My allowance was 500 MB. Most of the output was native binaries and debugging symbols, rather than the much smaller screenshots and diagrams.</p>
<p>Uploads were failing because the quota had been reached. Application checks could pass, then the workflow would fail during upload and block the dependent cross-platform backup cycle. I had to distinguish the checks that had passed from the validation that couldn't run.</p>
<p>The first reduction removed the large bundles and rebuilt from the tested commit in each job, keeping only the small encrypted backups needed between platforms. I then made the policy explicit. Actions artifact storage was for release-ready application binaries and, where useful, their debugging symbols. Test data, traces, screenshots, and intermediate output didn't belong there.</p>
<p>The final prototype workflow uploaded no artifacts. It passed the small synthetic encrypted backup through job outputs so the cross-platform restore exercise could continue. The resulting hosted run passed all seven jobs and reported zero artifacts. That transfer was specific to the small fixture, not a general mechanism for moving application databases.</p>
<p>This changed the earlier plan to retain generated architecture output as an artifact too. LikeC4 still ran during validation; generating its output didn't require uploading it on every run.</p>
<p>Asking the agent to retain evidence had produced a workflow whose storage needs hadn't been checked against the available allowance. Reviewing the pipeline needed to include what it kept and why. Otherwise the process intended to validate the application could prevent that validation from finishing.</p>
<h2>The proposed permissions boundary had another route around it</h2>
<p>For a live tracker-recovery trial, the agent proposed narrowly scoped credentials. I questioned whether that would provide the intended restriction. I'd already needed broader permissions for legitimate project work, and the existing authenticated GitHub CLI session was still available.</p>
<p>GitHub would enforce the permissions of the credential used for a request. Giving the controller a narrow credential wouldn't isolate an agent that could also use the broader session.</p>
<p>I kept the trial focused on whether the controller could recover after a GitHub update. The manual tracker adapter was fixed to one sandbox issue, one project, one marked comment, and two status transitions. It exercised losing the response after GitHub had accepted a write, then recovering without duplicating the comment or repeating an already completed mutation. The broader operator permissions were recorded explicitly.</p>
<p>That let me investigate recovery under supervision. It didn't establish a security boundary for unattended execution, which remained unapproved. That would need an isolated environment with only the intended credentials available.</p>
<p>The controller's process-recovery tests had a separate problem on Windows. A supervisor intended to survive the controller's termination wasn't producing its recovery receipt. This time the run reached its correction limit and stopped. I explicitly authorised another attempt. Detaching the supervisor resolved the tested failure, and the hosted checks subsequently passed across all three operating systems.</p>
<p>That was a concrete improvement over the early budget bookkeeping. The run stopped with an unresolved problem, and continuing required a decision from me.</p>
<p>This is definitely one I want to return to, as ideally agents should only be running with the exact access they need. For this supervised trial on a personal project, I deemed the risk acceptable with the controls I'd put in place.</p>
<h2>What I'd accepted</h2>
<p>After working through the prototypes, I was happy to move forward with Avalonia and .NET, SQLite3MC, the guided reconciliation approach, and the small controller I'd been testing under supervision. The workflow checks had passed on macOS, Windows, and Linux, and a backup had been restored through all three platforms.</p>
<p>The disposable shell still needed production boundaries, importer contracts, accessibility acceptance, security decisions, and packaging work. Passing the prototypes hadn't turned it into a finished financial application.</p>
<p>I also asked for an isolated trial with my real data and actual flow once the complete workflow was available, before release packaging. Synthetic fixtures meant development and validation could happen without exposing financial records. The later trial would check whether the application fitted the job I needed it for, with me operating it privately and keeping the records out of the agents, repository, and CI. I still wanted to try it with my own data before considering it ready for release.</p>
]]></content:encoded></item><item><title><![CDATA[Building an agentic workflow, part 1: deciding what done means]]></title><description><![CDATA[Setting up my own agentic development loop around dot-finance, with repository context, validation, independent review, and limits on correction attempts.]]></description><link>https://nullabletype.hashnode.dev/building-an-agentic-workflow-part-1-deciding-what-done-means</link><guid isPermaLink="true">https://nullabletype.hashnode.dev/building-an-agentic-workflow-part-1-deciding-what-done-means</guid><dc:creator><![CDATA[Steven Davies]]></dc:creator><pubDate>Tue, 20 Oct 2026 17:57:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/617c75a975401e57d6a87f3c/6cf8c2b6-6f93-4686-b065-45ba191469aa.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I still reconcile my credit-card spending manually across several spreadsheets. The calculations and automation I've added help, but it's still painful to do every month.</p>
<p>That's the reason for <a href="https://github.com/nullabletype/dot-finance">dot-finance</a>. I want an application that imports transactions, works out how much needs to come from different accounts, and records what's been settled. It will stay offline and I'll still make the transfers myself.</p>
<p>There's another part to the project though. I've used coding agents on plenty of bounded tasks, and full agentic workflows in my job as a software architect. I want to build my own development loop from scratch for personal projects, with dot-finance as a real application to test it against.</p>
<p>That means working out what happens around the coding. An agent needs enough context to pick up an issue in a fresh session, a way to check its work, and a limit on how long it can spend trying to correct a failure. If a run stops halfway through, I need to know where it got to without reconstructing the whole conversation.</p>
<p>Before writing application code, I started by putting those rules into the repository.</p>
<h2>Context that survives the conversation</h2>
<p>I began with a specification and asked Codex to review it and grill me on anything unclear before planning the work.</p>
<p>I've been using reusable agent skills for this, including ideas from <a href="https://github.com/mattpocock/skills">Matt Pocock's skills collection</a>. A skill gives the agent instructions for a particular kind of work, which saves rebuilding the same prompt each time. The local <code>grilling</code> skill asks one question at a time, gives a recommendation, and waits for a decision. I then used <code>to-issues</code> to turn the prototype work into <a href="https://github.com/nullabletype/dot-finance/issues">GitHub tickets</a>.</p>
<p>The conversation is useful for reaching decisions, but a later session needs to find the decisions without reading everything that led to them. I've split that context across three files.</p>
<ul>
<li><p><a href="https://github.com/nullabletype/dot-finance/blob/main/spec-file.md"><code>spec-fil</code></a>Setting up my own agentic development loop around dot-finance, with repository context, validation, independent review, and limits on correction attempts.<a href="https://github.com/nullabletype/dot-finance/blob/main/spec-file.md"><code>e.md</code></a> describes what the application must do.</p>
</li>
<li><p><a href="https://github.com/nullabletype/dot-finance/blob/main/development-workflow.md"><code>development-workflow.md</code></a> describes how an issue moves through implementation, validation, review, and integration.</p>
</li>
<li><p><a href="https://github.com/nullabletype/dot-finance/blob/main/AGENTS.md"><code>AGENTS.md</code></a> gives an agent the operating rules for the repository.</p>
</li>
</ul>
<p>For example, staying offline is an application constraint. Using synthetic development data is a rule for doing the work. A missing platform check is unfinished validation. All three need to survive the session in which they were discussed, and each belongs somewhere an agent can find it again.</p>
<p>This also gives me something concrete to review when the process needs changing. I can update the relevant rule rather than leave a correction buried in a chat and hope the next run sees it.</p>
<h2>One issue at a time</h2>
<p>The initial loop is sequential. It selects one ready issue with completed dependencies, works on an isolated branch, runs the required checks, and sends the result through independent review before integration.</p>
<img src="https://files.nullabletype.dev/posts/dot-finance-agentic-loop-v3.svg" alt="A workflow beginning with one ready issue, moving through implementation, checks and independent review, and either recording evidence, returning for correction, or stopping when the correction budget is exhausted." style="display:block;margin:0 auto" />

<p><em>The proposed loop at this stage of the project. The workflow is documented; the controller hasn't been built.</em></p>
<p>Starting with one issue keeps the coordination manageable. There's one branch to inspect and one set of expected outcomes. I can work out whether the basic loop behaves properly before adding concurrent work and the extra problems that brings.</p>
<p>The issue needs to say how I'll know the work is complete. “Add encrypted backup” leaves too much open. Saving synthetic data on one supported platform, restoring it on another, and checking that the expected state survived gives the agent a result to demonstrate.</p>
<p>Opening a pull request doesn't satisfy that requirement. A green build might not satisfy it either, if the required behaviour was never exercised. The issue stays open until the agreed validation and integration are complete.</p>
<p>The evidence also needs to identify the commit it came from. If a review leads to code changes, the affected checks have to run again. A Windows check that passed before those changes can't simply be carried forward because it's on the same branch.</p>
<p>It's a small bookkeeping requirement with a practical consequence. After an interruption, I need to be able to tell which code passed which checks, and which checks are still pending.</p>
<h2>Putting a limit on corrections</h2>
<p>In my earlier AI field notes, I described using Gemini to help set up an Octopus Deploy instance. I kept feeding errors back into the conversation, and it eventually suggested running Octopus on PostgreSQL. By then I should have stepped back, checked what was missing from the context, and started again from a better position.</p>
<p>I don't want this loop to depend on me noticing that point every time.</p>
<p>For the prototype phase, I've set an initial limit of three correction attempts or 90 minutes of active agent work, whichever comes first. Those numbers are provisional. They give the first runs a stopping point while I find out whether the limits are useful in practice.</p>
<p>When the budget runs out, the controller should return enough information to investigate or continue deliberately. That includes the issue, branch and commit, checks completed, evidence collected, correction count, remaining problem, and next action. Continuing needs an explicit new budget.</p>
<p>An interruption needs the same care. Restarting a run mustn't reset its correction count or turn an unfinished check into a pass. The plan is to keep GitHub as the record of the work, with a local checkpoint holding enough execution state to resume accurately.</p>
<p>The controller also needs to distinguish a code failure from an infrastructure problem. If a hosted runner is unavailable, editing the application won't help. That platform's validation should remain pending, with the blocker recorded. Otherwise the loop could spend its correction budget changing code that hasn't been shown to be wrong.</p>
<p>These are behaviours I need to test in the <a href="https://github.com/nullabletype/dot-finance/tree/main/prototypes/ControllerTrial">controller itself</a>. The prototype plan includes a deliberately failing check, an interrupted run, an unavailable runner, and a review rejection. A successful application build wouldn't tell me whether any of those paths work.</p>
<h2>Reviewing the tests and the architecture</h2>
<p>Independent review needs to look beyond whether the implementation compiles. Code and tests written together can share the same misunderstanding, so the reviewer needs the requirement, the diff, and the validation evidence. Part of that review is deciding whether the tests would catch a meaningful error in the required behaviour.</p>
<p>I've also asked for <a href="https://github.com/nullabletype/dot-finance/tree/main/docs/architecture">LikeC4 architecture diagrams</a> alongside the implementation. I've been a long-term fan of the C4 model and the consistency it brings to architectural discussions. This project gives me a chance to try architecture as code while an agent is making the implementation changes.</p>
<p>The proposed rule is to update the LikeC4 model in the same pull request when component responsibilities, dependencies, storage, or significant flows change. If a change has no architectural effect, the pull request can say so.</p>
<p>The build is intended to validate the model and generate the diagrams. That would catch an invalid model, but it wouldn't establish that the diagram accurately describes the C# code. Checking that agreement remains part of review. Otherwise I could end up with a passing build and an increasingly misleading description of the application.</p>
<p>At the end of this setup stage, I had a specification, prototype tickets, and workflow rules. There was no application or controller yet. The first prototype was planned to establish the desktop and LikeC4 build foundation, followed by investigations into encrypted storage, restoration across platforms, and the guided reconciliation flow. Alongside those application checks, the controller still had to demonstrate that it could stop within its budget and resume without losing track of unfinished validation.</p>
]]></content:encoded></item><item><title><![CDATA[AI Field Notes 2026]]></title><description><![CDATA[There is no doubt that AI has rocked the world of coding considerably over the last couple of years. One thing that has become apparent, through keeping up…]]></description><link>https://nullabletype.hashnode.dev/ai-field-notes-2026</link><guid isPermaLink="true">https://nullabletype.hashnode.dev/ai-field-notes-2026</guid><dc:creator><![CDATA[Steven Davies]]></dc:creator><pubDate>Wed, 01 Jul 2026 21:31:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/617c75a975401e57d6a87f3c/4896c706-5b17-45a5-8419-3348c15a4816.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There is no doubt that AI has rocked the world of coding considerably over the last couple of years. One thing that has become apparent, through keeping up with the whirlwind of changes, is that people are still figuring out how to use these tools effectively. It seems like every video I watch, or article I read, people are swearing they've found the killer approach.</p>
<p>I have noticed through all the noise though, that there are some key concepts that seem common regardless of which approach you take. This isn't comprehensive, and is likely to change over time, but my key observations in July 2026 are:</p>
<h2>Clarity is king</h2>
<p>I recently watched this excellent <a href="https://www.youtube.com/watch?v=yhGzXULZkEw">TED Talk by Rainer Stropek</a>. His closing remark is "I am not just a coder, I am a developer and my new programming language is clarity". The talk is well worth a watch for various reasons, but this statement definitely struck a chord with me.</p>
<p>Clarity, when working with AI agents, is key. This isn't a new thing though, as if you were working with a team on delivering a coding project, having a shared context and understanding of the goal and approach is key. That doesn't change when working with AI. Having that shared understanding of not just the task, but the goal and the process leading there will give you much better results than just stating a task and expecting great results.</p>
<p>Rather than just state the task, state the goal you want to achieve and the key criteria or guardrails to get there. Use the LLM to help plan and build your spec before letting it loose. A lot of harnesses have planning modes, so give them a try. Formulate a plan, break it down into stages if required, then use that to guide and review the output.</p>
<p>"<a href="https://github.com/mattpocock/skills/blob/main/skills/productivity/grill-me/SKILL.md">Grill me relentlessly</a>", as Matt Pocock recommends, to work towards that shared understanding.</p>
<pre><code class="language-text">Weak:
Add task cancellation to the CLI.

Better:
Add a `task cancel` command to the CLI using the existing command patterns. 

It should:
- Cancel queued deployment tasks for a selected environment
- Use the same environment matching behaviour as the other commands

Ensure you:
- Include unit tests and live integration tests
- Update any user-facing help text if the command surface changes
- Run the unit test suite, including integration tests
- Provide a summary of your changes and validation
</code></pre>
<h2>Context is also king</h2>
<p>A follow-up point from the above: be surgical about the context you provide. Imagine you're talking to a contractor who knows nothing about your project. Be clear on the technical stack and constraints. Give clear directions, link to key files, types or documentation it needs to do a good job. Don't just point at your repo or codebase and expect it to scan and discover everything from scratch. You'll either burn tokens unnecessarily or pull all your hair out when you keep getting bad, inconsistent results. Even worse, both. High signal, low noise.</p>
<p>Prefer smaller, reviewable tasks. Give clear boundaries and observable success criteria where possible.</p>
<pre><code class="language-text">Weak:
Look through the repo and work out how commands are built.

Better:
The existing command patterns are in `src/ShipItSharp.Console/Commands`. 

Start with `BaseCommand`, `Environment`, and `Channel` as examples. 

Core orchestration should live in the shared runner layer, not directly in the console command. 
</code></pre>
<h2>You're not just stuck in the terminal</h2>
<p>I picked this up from <a href="https://www.youtube.com/watch?v=fdbXNWkpPMY">a talk by Lucas Meijer</a>, who offered a simple tip that made my comprehension and efficiency so much better. Ask for a single-page HTML summary. That's it, but it's amazingly powerful in the right situation.</p>
<p>One example use case is I asked Codex to "grill me" on a complex goal and it generated 60 questions for me to review. Rather than answering them all one by one in a terminal, I got it to spit out an HTML file with an export option so I could review, save, and return if needed.</p>
<p>I've also used it to generate reviews of code, produce a detailed plan of action or even show me design choices to choose between when I've been unsure.</p>
<p>Obviously, don't overuse this as it can burn tokens, but in the right contexts it can produce a much easier response to comprehend and lead you to a more efficient goal.</p>
<h2>Chat context is expensive</h2>
<p>It's a general recommendation that you keep to a low number of messages per chat or session. Why? Every time you send a message, the entire previous context is also sent. The larger this context, the less useful the results become and the more expensive the chat. Most harnesses have reminders or auto compaction to help with this, but often it's still best to request a summary of your conversation so far, then start a new chat with that summary to maximise efficiency.</p>
<p>On a similar note, if your instructions were unclear and you got a duff response back, don't scorn the AI tool and tell it to try again. Consider editing your original message so it's clearer and then resend. It keeps your context shorter, will give you a more refined response and will burn fewer tokens.</p>
<h2>Get AI to validate its own work</h2>
<p>Before you set your agent off on a series of tasks, make sure you define sensible validation for the result. Think about what you would expect if you were doing the work yourself. Unit tests that pass covering specific areas? Second pass review? Integration tests? Browser validation? Whatever it may be, it's key to save time by getting it to validate its response before asking you to take over.</p>
<p>One tip I picked up from a colleague is to task the agent with continuing to refine until it's 95% confident on an answer. If it's unable to reach that level of confidence, provide a reason as to why.</p>
<p>A pattern I’ve started using is asking Codex to do a second-pass review against the original goal, not just against compiler errors. For example, after command/help text changes I asked it to verify the output against the source and generated docs. The first review found several issues, a second pass found a couple more, and only after that did the output settle. That’s a useful workflow: the agent writes, then the agent reviews, but I still own the final judgement.</p>
<p>Another example is I recently used Codex to add new task-management commands to <a href="https://shipitsharp.app">ShipItSharp</a>. The useful part wasn’t that it generated the code. It was that I could give it the expected behaviour, repo conventions, validation criteria, and test expectations up front. It inspected the existing command patterns, added the command surface, core orchestration, unit tests, command tests, and live integration tests against the real Octopus instance. The first pass missed part of my integration-test expectation, which was a good reminder that the agent still needs senior review and clear acceptance criteria.</p>
<h2>The two-strike rule</h2>
<p>When an agent's code throws an error, give it one shot to fix it. If it fails a second time, step away from the prompt window. Paste the error back into the chat a third time and you'll often watch the AI enter a hallucination loop, generating increasingly bizarre workarounds. Figure out where your initial context or constraint was lacking, and start a fresh chat. Know when to cut your losses.</p>
<p>A perfect example of this is when I was working on <a href="https://shipitsharp.app">ShipItSharp</a>. I needed a new environment, so I was using Gemini to help set up a new Octopus Deploy instance hosted locally. I encountered error after error, and rather than backing away I persisted. In the end, it hallucinated that Octopus Deploy can run on PostgreSQL. After calling it out, its response was:</p>
<blockquote>
<p>I owe you a massive, deeply embarrassing apology.</p>
</blockquote>
<p>Lesson learned.</p>
<h2>Think of using AI agents as a "layer up"</h2>
<p>This calls back to <a href="https://www.youtube.com/watch?v=yhGzXULZkEw">Rainer Stropek's talk</a>, where he talks about the shift from being on the ground writing code to moving another layer up to orchestrate the tool to do the boilerplate stuff. Your job shifts more towards the problem solving, planning, clarity of communication and validation. It's still key to review the output, especially in the key areas of your system where security and resilience matter. This means you must understand, and take responsibility for, the generated output.</p>
<p>A good example for me was release workflow work. I didn't need Codex just to edit YAML. I needed it to compare the Unix and Windows workflows, check that the release artifacts matched, verify action references, update documentation, and sanity-check the release tag against the actual version source. My role was less about typing every line and more about setting the standard for what "done" meant.</p>
<p>It honestly reminds me of earlier in my career. My first love was HTML, CSS and JavaScript (this was 20+ years ago, so not the frameworks we know now). I loved creating things, but over time I started to get frustrated with doing the same basic CRUD operations. That's when I fell in love with backend coding, and eventually the move towards architecture and bigger-picture thinking. The key through all of this is the love of problem solving. Seeing an issue, finding a solution, and building a solution.</p>
<p>AI tools are like that next level up, where the key things that make you a great developer are still absolutely relevant. Anyone can vibe code something, but building something resilient, secure, safe and scalable for thousands if not millions of users still requires strong development skills.</p>
<h2>AI is not human, don't treat it like it is</h2>
<p>It may sound harsh, but its true. It's only natural to want to personify the AI tools we use. After all, it often responds to you as if it was a friend or colleague. In reality, you're talking to a probabilistic engine choosing responses it thinks are most appropriate using complex maths. It has no feelings or empathy. Keep operational prompts concise.</p>
<p>Most harnesses these days have an option to change its personality. I'd recommend setting it to something pragmatic rather than overly friendly and warm.</p>
<p>Do you have any tips you've found useful? Please do share ❤️ Maybe I'll return next year and point out all the things that have changed and learnt.</p>
]]></content:encoded></item><item><title><![CDATA[Proxy any class by interface in C# with DispatchProxy]]></title><description><![CDATA[I was recently tasked with figuring out a way to performance test database calls being made via NHibernate to PostgreSQL. As far as I could find…]]></description><link>https://nullabletype.hashnode.dev/proxy-any-class-by-interface-in-c-with-dispatchproxy</link><guid isPermaLink="true">https://nullabletype.hashnode.dev/proxy-any-class-by-interface-in-c-with-dispatchproxy</guid><category><![CDATA[C#]]></category><category><![CDATA[proxy]]></category><category><![CDATA[Programming Tips]]></category><category><![CDATA[.NET]]></category><dc:creator><![CDATA[Steven Davies]]></dc:creator><pubDate>Fri, 22 Oct 2021 00:18:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1635553207222/XzLhZnvb-.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I was recently tasked with figuring out a way to performance test database calls being made via <code>NHibernate</code> to <code>PostgreSQL</code>. As far as I could find, <code>NHibernate</code> doesn't expose any really easy ways to intercept and manually log both the <code>SQL</code> and execution times for each query. </p>
<p>After a little more digging, I came across a handy lesser-known tool that allows you to easily proxy any interface, be it user-defined or from a third party library. This tool lives in the <a target="_blank" href="https://www.nuget.org/packages?q=System.Reflection.DispatchProxy"><code>System.Reflection.DispatchProxy</code></a> nuget package. With a little code, you can wrap any instance of a class that implements an interface with a proxy to not only log but also manipulate both arguments and returned data.</p>
<h2 id="example">Example</h2>
<p>Take this simple class and interface.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">interface</span> <span class="hljs-title">IHello</span>
{
    <span class="hljs-function"><span class="hljs-keyword">bool</span> <span class="hljs-title">SayHello</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> name</span>)</span>;
}

<span class="hljs-keyword">class</span> <span class="hljs-title">Hello</span> : <span class="hljs-title">IHello</span>
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">bool</span> <span class="hljs-title">SayHello</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> name</span>)</span>
    {
        Console.WriteLine(<span class="hljs-string">$"Hello <span class="hljs-subst">{name}</span>"</span>);
        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    }
}
</code></pre>
<p>Nothing special, only one method exposed which takes a simple name parameter, writes out a string to the console and returns a bool indicating success.</p>
<p>Say, although contrived in this example, we wanted to record calls going to <code>SayHello</code>.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">HelloDispatchProxy</span>&lt;<span class="hljs-title">T</span>&gt; : <span class="hljs-title">DispatchProxy</span> <span class="hljs-keyword">where</span> <span class="hljs-title">T</span> : <span class="hljs-keyword">class</span>, <span class="hljs-title">IHello</span>
{
    <span class="hljs-keyword">private</span> IHello Target { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">object</span> <span class="hljs-title">Invoke</span>(<span class="hljs-params">MethodInfo targetMethod, <span class="hljs-keyword">object</span>[] args</span>)</span>
    {
        <span class="hljs-comment">// Code here to track time, log call etc.</span>
        <span class="hljs-keyword">var</span> result = targetMethod.Invoke(Target, args);
        <span class="hljs-keyword">return</span> result;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> T <span class="hljs-title">CreateProxy</span>(<span class="hljs-params">T target</span>)</span>
    {
        <span class="hljs-keyword">var</span> proxy = Create&lt;T, HelloDispatchProxy&lt;T&gt;&gt;() <span class="hljs-keyword">as</span> HelloDispatchProxy&lt;T&gt;;
        proxy.Target = target;
        <span class="hljs-keyword">return</span> proxy <span class="hljs-keyword">as</span> T;
    }
}
</code></pre>
<p>This is our <code>DispatchProxy</code>, <code>HelloDispatchProxy</code>. Usage is pretty simple.</p>
<pre><code class="lang-csharp">IHello hello = HelloDispatchProxy&lt;IHello&gt;.CreateProxy(<span class="hljs-keyword">new</span> Hello());
</code></pre>
<p>The <code>CreateProxy</code> method calls <code>Create&lt;T&gt;</code> on the base abstract class to generate a proxy object for you. The target instance is then assigned to the <code>Target</code> property before returning the proxy cast to type <code>T</code> (<code>IHello</code> in this instance).</p>
<p>Any method that is then called on the proxied <code>IHello</code> instance will trigger <code>Invoke()</code>. It's your proxies responsibility to call the target method, pass through the parameters and return the result. In this method, you can do whatever you like, and you'll also have access to your original <code>Target</code> instance.</p>
<p>You can see a complete example of this over on my <a target="_blank" href="https://github.com/nullabletype/scratchpad/blob/main/DispatchProxyExample/DispatchProxyExample/HelloDispatchProxy.cs">GitHub</a></p>
<blockquote>
<p>With great power comes great responsibility</p>
</blockquote>
<p>This allows you quite a lot of control, so be careful what you do with <code>args</code> and the returned result or you can cause some unexpected behaviour.</p>
<h2 id="performance-testing">Performance testing</h2>
<p><code>DispatchProxy</code> lives under the <code>System.Reflection</code> namespace. Because of this, I was curious about what overhead is introduced by this approach. What better way to understand than to test it myself.</p>
<p>I set up a <a target="_blank" href="https://github.com/nullabletype/scratchpad/blob/main/DispatchProxyExample/DispatchProxyExample/Program.cs">slightly over-engineered set of tests</a> to give a raw comparison between calls to both an unproxied and proxied instance. I ran these multiple times to get an average before analysing the results. </p>
<pre><code>    <span class="hljs-attribute">100000</span> W/O    <span class="hljs-number">100000</span> W    % Increase
    <span class="hljs-attribute">2233</span>.<span class="hljs-number">0230</span>    <span class="hljs-number">2382</span>.<span class="hljs-number">8835</span>    <span class="hljs-number">6</span>.<span class="hljs-number">71</span>%
    <span class="hljs-attribute">2235</span>.<span class="hljs-number">5306</span>    <span class="hljs-number">2398</span>.<span class="hljs-number">1216</span>    <span class="hljs-number">7</span>.<span class="hljs-number">27</span>%
    <span class="hljs-attribute">2236</span>.<span class="hljs-number">2449</span>    <span class="hljs-number">2386</span>.<span class="hljs-number">4416</span>    <span class="hljs-number">6</span>.<span class="hljs-number">72</span>%
    <span class="hljs-attribute">2236</span>.<span class="hljs-number">8529</span>    <span class="hljs-number">2389</span>.<span class="hljs-number">2090</span>    <span class="hljs-number">6</span>.<span class="hljs-number">81</span>%
    <span class="hljs-attribute">2239</span>.<span class="hljs-number">9692</span>    <span class="hljs-number">2402</span>.<span class="hljs-number">4627</span>    <span class="hljs-number">7</span>.<span class="hljs-number">25</span>%
    <span class="hljs-attribute">2227</span>.<span class="hljs-number">0108</span>    <span class="hljs-number">2367</span>.<span class="hljs-number">0306</span>    <span class="hljs-number">6</span>.<span class="hljs-number">29</span>%
    <span class="hljs-attribute">2237</span>.<span class="hljs-number">8432</span>    <span class="hljs-number">2384</span>.<span class="hljs-number">8429</span>    <span class="hljs-number">6</span>.<span class="hljs-number">57</span>%
    <span class="hljs-attribute">2233</span>.<span class="hljs-number">5528</span>    <span class="hljs-number">2392</span>.<span class="hljs-number">8394</span>    <span class="hljs-number">7</span>.<span class="hljs-number">13</span>%
    <span class="hljs-attribute">2238</span>.<span class="hljs-number">4110</span>    <span class="hljs-number">2398</span>.<span class="hljs-number">3124</span>    <span class="hljs-number">7</span>.<span class="hljs-number">14</span>%
    <span class="hljs-attribute">2235</span>.<span class="hljs-number">7398</span>    <span class="hljs-number">2388</span>.<span class="hljs-number">1340</span>    <span class="hljs-number">6</span>.<span class="hljs-number">82</span>%
    <span class="hljs-attribute">2262</span>.<span class="hljs-number">3454</span>    <span class="hljs-number">2380</span>.<span class="hljs-number">0595</span>    <span class="hljs-number">5</span>.<span class="hljs-number">20</span>%
    <span class="hljs-attribute">2340</span>.<span class="hljs-number">0243</span>    <span class="hljs-number">2385</span>.<span class="hljs-number">0553</span>    <span class="hljs-number">1</span>.<span class="hljs-number">92</span>%
    <span class="hljs-attribute">2249</span>.<span class="hljs-number">3693</span>    <span class="hljs-number">2383</span>.<span class="hljs-number">6925</span>    <span class="hljs-number">5</span>.<span class="hljs-number">97</span>%
    <span class="hljs-attribute">2246</span>.<span class="hljs-number">9835</span>    <span class="hljs-number">2381</span>.<span class="hljs-number">5070</span>    <span class="hljs-number">5</span>.<span class="hljs-number">99</span>%
    <span class="hljs-attribute">2240</span>.<span class="hljs-number">7711</span>    <span class="hljs-number">2381</span>.<span class="hljs-number">4675</span>    <span class="hljs-number">6</span>.<span class="hljs-number">28</span>%
    <span class="hljs-attribute">2232</span>.<span class="hljs-number">5296</span>    <span class="hljs-number">2374</span>.<span class="hljs-number">0465</span>    <span class="hljs-number">6</span>.<span class="hljs-number">34</span>%
    <span class="hljs-attribute">2248</span>.<span class="hljs-number">0095</span>    <span class="hljs-number">2359</span>.<span class="hljs-number">8636</span>    <span class="hljs-number">4</span>.<span class="hljs-number">98</span>%
    <span class="hljs-attribute">2250</span>.<span class="hljs-number">2518</span>    <span class="hljs-number">2394</span>.<span class="hljs-number">8422</span>    <span class="hljs-number">6</span>.<span class="hljs-number">43</span>%
    <span class="hljs-attribute">2234</span>.<span class="hljs-number">7757</span>    <span class="hljs-number">2387</span>.<span class="hljs-number">8114</span>    <span class="hljs-number">6</span>.<span class="hljs-number">85</span>%
    <span class="hljs-attribute">2238</span>.<span class="hljs-number">7645</span>    <span class="hljs-number">2387</span>.<span class="hljs-number">8047</span>    <span class="hljs-number">6</span>.<span class="hljs-number">66</span>%


    <span class="hljs-attribute">0</span>.<span class="hljs-number">00112245</span>    <span class="hljs-number">0</span>.<span class="hljs-number">001192661</span>    <span class="hljs-number">6</span>.<span class="hljs-number">27</span>%
</code></pre><p>So on average, the execution took an extra 6 or so percent with just the introduction of the proxy. Bear in mind in this current state, the proxy is doing nothing useful at all, but this helps put into context the efficiency.</p>
<p>But if you were to add something slow like a blocking console write line in the proxy, the % increase shoots up from 6.27% to over 100%.</p>
<pre><code class="lang-csharp">System.Console.WriteLine(<span class="hljs-string">$"Going to call <span class="hljs-subst">{targetMethod.Name}</span>"</span>);
</code></pre>
<p>The most important part with the performance impact is what you decide to do in the <code>Invoke</code> method, something you'll need to benchmark yourself.</p>
<p>In all but the highest performance-critical applications, the overhead of the proxy itself is minor. If you take for example proxying 2-3 DB calls in the context of something like a HTTP web request or message handler the impact is negligible. In fact, in my testing with a real-world application, the numbers are within an acceptable margin of error even when tracing into an <a target="_blank" href="https://en.wikipedia.org/wiki/Application_performance_management">APM</a> tool.</p>
<h2 id="nhibernate-and-postgresql">NHibernate &amp; PostgreSQL</h2>
<p>If you're specifically interested in collecting information from <code>NHibernate</code> as I was, you can do this by wrapping the <code>IDBCommand</code> in a custom driver implementation.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">ProfiledNpgsqlDriver</span> : <span class="hljs-title">NpgsqlDriver</span> {
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> IDbCommand <span class="hljs-title">CreateCommand</span>(<span class="hljs-params"></span>)</span> {
        <span class="hljs-keyword">var</span> command = <span class="hljs-keyword">base</span>.CreateCommand();

        <span class="hljs-keyword">if</span> (ConfigurationManager.AppSettings[<span class="hljs-string">"EnableDBTracing"</span>]?.ToLower() == <span class="hljs-string">"true"</span>) {
            command = ProfiledDbCommandDispatchProxy&lt;IDbCommand&gt;.CreateProxy(command);
        }

        <span class="hljs-keyword">return</span> command;
    }
}
</code></pre>
<p>You can access the command being executed from the target <code>IDBCommand</code>'s <code>Command</code> property and record time etc. with a <code>StopWatch</code>. I went a step further and implemented the proxy as an <a target="_blank" href="https://docs.microsoft.com/en-us/dotnet/api/system.iobservable-1?view=net-5.0"><code>IObservable&lt;T&gt;</code></a> so I can handle it differently depending on the use case.</p>
<h2 id="wrap-up">Wrap up</h2>
<p>Hopefully, you found this interesting. Can you think of any good use cases for this, or have any comments? Would love to hear them ❤</p>
]]></content:encoded></item><item><title><![CDATA[Auto OpenVPN reconnect and killswitch for VPNs with dynamic IPs]]></title><description><![CDATA[If you’re like me and run a home Linux server, you may wish to add some extra security in the form of a VPN connection to ensure traffic is encrypted. In…]]></description><link>https://nullabletype.hashnode.dev/auto-openvpn-reconnect-and-killswitch-for-vpns-with-dynamic-ips</link><guid isPermaLink="true">https://nullabletype.hashnode.dev/auto-openvpn-reconnect-and-killswitch-for-vpns-with-dynamic-ips</guid><category><![CDATA[Security]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Bash]]></category><category><![CDATA[shell]]></category><dc:creator><![CDATA[Steven Davies]]></dc:creator><pubDate>Mon, 04 Oct 2021 00:12:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1635552918937/pR9ksypzC.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="the-problem">The Problem</h3>
<p>If you’re like me and run a home Linux server, you may wish to add some extra security in the form of a VPN connection to ensure traffic is encrypted. In my case, I use a Raspberry Pi to backup things like RAW images from my camera to an off-site location. The extra security gives me a little peace of mind, especially as the server generally uses a cellular connection.</p>
<p>I’ve been using <code>UFW</code> to work as a killswitch and only allow traffic via the VPN connection, but one issue I've had is with my trusted VPN provider is that they regularly rotate IP addresses. <code>UFW</code> uses <code>iptables</code> under the hood, meaning it can only be configured to use IP addresses explicitly and not a domain name. This has left my backup machine without an internet connection fairly frequently.</p>
<h3 id="the-solution">The Solution</h3>
<p>Just to set this straight from the start, I’m no UNIX genius. I spend most of my time working with Windows machines and use Linux more as a hobbyist. That been said, I’ve written my fair share of code/scripts.</p>
<p>My solution is pretty simple; when disconnects happen:</p>
<ol>
<li>fetch a fresh list of IP’s from my internal DNS server.</li>
<li>Update <code>UFW</code> .</li>
<li>Update the <code>OpenVPN</code>connection file and reconnect.</li>
</ol>
<p>I thought I’d share my approach, as I failed finding any other solution online so this may be of help to others.</p>
<p>The first part is fetching the list of domains. To do this, I use <a target="_blank" href="https://downloads.isc.org/isc/bind9/cur/9.17/doc/arm/html/manpages.html#dig-dns-lookup-utility">dig</a>.</p>
<pre><code class="lang-bash">ipoutput=$(dig <span class="hljs-variable">$1</span> +short)

<span class="hljs-keyword">if</span> [ -z “<span class="hljs-variable">$ipoutput</span>” ]; <span class="hljs-keyword">then</span>  
  <span class="hljs-built_in">echo</span> “No ips found <span class="hljs-keyword">for</span> <span class="hljs-variable">$1</span>”  
  <span class="hljs-built_in">exit</span> 1  
<span class="hljs-keyword">fi</span>

<span class="hljs-built_in">echo</span> “Found ips <span class="hljs-variable">$ipoutput</span>”

<span class="hljs-built_in">readarray</span> -t ips &lt;&lt;&lt;”<span class="hljs-variable">$ipoutput</span>”
</code></pre>
<p>This gives me a nice list of IP address for the specified domain.</p>
<p>Now I know the correct ip addresses, I need to make use of them. The first job is to update <code>UFW</code>. I decided to use a template file so I can set it up as I want for each machine. This may not be the most elegant solution, but its pretty flexible.</p>
<pre><code class="lang-bash">ufwoutput=’’

<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> “<span class="hljs-variable">${ips[@]}</span>”  
<span class="hljs-keyword">do</span>  
  :  
  ufwoutput+=$”<span class="hljs-comment">### tuple ### allow udp any $i any 0.0.0.0\\/0 out\\n”  </span>
  ufwoutput+=$”-A ufw-user-output -p udp -d <span class="hljs-variable">$i</span> -j ACCEPT\\n\\n”  
<span class="hljs-keyword">done</span>

<span class="hljs-keyword">if</span> <span class="hljs-built_in">test</span> -f “<span class="hljs-variable">$dir_path</span>/user.rules.tmp”; <span class="hljs-keyword">then</span>  
  <span class="hljs-built_in">echo</span> “Deleting tmp file…”  
  rm “<span class="hljs-variable">$dir_path</span>/user.rules.tmp”  
<span class="hljs-keyword">fi</span>

cp “<span class="hljs-variable">$dir_path</span>/user.rules.template” “<span class="hljs-variable">$dir_path</span>/user.rules.tmp”

sed -i -e “s/\[content\]/<span class="hljs-variable">${ufwoutput}</span>/g” “<span class="hljs-variable">$dir_path</span>/user.rules.tmp” “<span class="hljs-variable">$dir_path</span>/user.rules.tmp”

mv “<span class="hljs-variable">$dir_path</span>/user.rules.tmp” /etc/ufw/user.rules

<span class="hljs-built_in">echo</span> “Reloading ufw”  
ufw reload
</code></pre>
<p>The script builds up a string containing the correct formatted lines for the <code>user.rules</code> file for <code>UFW</code>, creates a new copy of the template, replaces <code>[content]</code> in the file using <code>sed</code> then moves the file into the correct place (for ubuntu). The final thing to do is <code>ufw reload</code> to reload the <code>user.rules</code> file.</p>
<p>That’s <code>UFW</code> taken care of, time for <code>OpenVPN</code> using a similar method</p>
<pre><code class="lang-bash">openvpnoutput=’’

<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> “<span class="hljs-variable">${ips[@]}</span>”  
<span class="hljs-keyword">do</span>  
:  
  openvpnoutput+=$”remote <span class="hljs-variable">$i</span> 1198\\n”  
<span class="hljs-keyword">done</span>

<span class="hljs-keyword">if</span> <span class="hljs-built_in">test</span> -f “<span class="hljs-variable">$dir_path</span>/openvpn.ovpn.tmp”; <span class="hljs-keyword">then</span>  
  <span class="hljs-built_in">echo</span> “Deleting tmp file…”  
  rm “<span class="hljs-variable">$dir_path</span>/openvpn.ovpn.tmp”  
<span class="hljs-keyword">fi</span>

cp “<span class="hljs-variable">$dir_path</span>/openvpn.ovpn.template” “<span class="hljs-variable">$dir_path</span>/openvpn.ovpn.tmp”

sed -i -e “s/\[content\]/<span class="hljs-variable">${openvpnoutput}</span>/g” “<span class="hljs-variable">$dir_path</span>/openvpn.ovpn.tmp” “<span class="hljs-variable">$dir_path</span>/openvpn.ovpn.tmp”

mv “<span class="hljs-variable">$dir_path</span>/openvpn.ovpn.tmp” “openvpn.ovpn”
</code></pre>
<p>This works in a similar way to the <code>UFW</code> code, replacing a <code>[content]</code> placeholder in the <code>.opvn</code> template file.</p>
<p>So that’s all the code to update <code>UFW</code> and <code>OpenVPN</code>. The next thing to do is to manage the connection. I do this via a simple script called from a <code>cron</code> job.</p>
<pre><code class="lang-bash"><span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">getStatus</span></span> () {  
  <span class="hljs-built_in">echo</span> “Attempting to get device status…”  
  ip address show | grep <span class="hljs-variable">$1</span> &amp;&amp; <span class="hljs-built_in">return</span> 1  
  <span class="hljs-built_in">return</span> 0  
}
</code></pre>
<p>This is a pretty straightforward function. <code>grep</code> the output from <code>ip address show</code> to see if the <code>OpenVPN</code> connection is live. Return either <code>1</code>for success or <code>0</code>for failure.</p>
<p>The last thing to do is to check the result and connect if required.</p>
<pre><code class="lang-bash">getStatus <span class="hljs-variable">$device</span>  
<span class="hljs-keyword">if</span> [[ $? == 0 ]]; <span class="hljs-keyword">then</span>  
  <span class="hljs-built_in">echo</span> “openvpn is not connected!”  
  <span class="hljs-built_in">echo</span> “Reconnecting!”

  <span class="hljs-comment">#Update config  </span>
  updateIps <span class="hljs-variable">$domain</span> &amp;&gt; <span class="hljs-variable">$dir_path</span>/renew.out &amp;<span class="hljs-built_in">disown</span>

  openvpn — config “<span class="hljs-variable">$dir_path</span>/openvpn.ovpn” &amp;&gt;<span class="hljs-variable">$dir_path</span>/out.out &amp;<span class="hljs-built_in">disown</span>  
<span class="hljs-keyword">else</span>  
  <span class="hljs-built_in">echo</span> “openvpn is connected!”  
<span class="hljs-keyword">fi</span>
</code></pre>
<p>If the output from <code>getStatus</code> is <code>0</code>, then it will call to update the IP addresses and trigger a new <code>OpenVPN</code> connection with <code>&amp;disown</code> to not wait for the connection to exit.</p>
<p>Now that’s all in place, all that’s needed to run the script automatically is a <code>cron</code> job to check as frequently as you wish.</p>
<pre><code class="lang-bash">*/1 * * * * <span class="hljs-built_in">cd</span> /home/account/openvpn &amp;&amp; ./auto-connect.sh “my.vpn.domain” “tun0” &gt; out.out
</code></pre>
<p>If this is useful to you, rather than having to copy &amp; paste the code I’ve wrapped all this up into a <a target="_blank" href="https://github.com/nullabletype/openvpntools">single script available on GitHub</a>.</p>
<h3 id="wrap-up">Wrap Up</h3>
<p>As I say, I’m no Linux sysadmin but this script solves a problem for me and has been working well for the last few months. If anyone has any suggestions for improvements or comments I’d love to hear them. Even better, <a target="_blank" href="https://github.com/nullabletype/openvpntools">fork the repo on GitHub</a> and pop in a merge request ❤</p>
]]></content:encoded></item><item><title><![CDATA[The imposter in the room...]]></title><description><![CDATA[I still remember back in 2006 when I joined my first job in the tech industry after achieving my University degree. I was only the second C# developer at…]]></description><link>https://nullabletype.hashnode.dev/the-imposter-in-the-room</link><guid isPermaLink="true">https://nullabletype.hashnode.dev/the-imposter-in-the-room</guid><category><![CDATA[personal]]></category><category><![CDATA[impostor syndrome]]></category><category><![CDATA[Self Improvement ]]></category><category><![CDATA[learning]]></category><dc:creator><![CDATA[Steven Davies]]></dc:creator><pubDate>Sun, 03 Oct 2021 00:16:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1635553049111/VOUmhgFRX.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I still remember back in 2006 when I joined my first job in the tech industry after achieving my University degree. I was only the second C# developer at the company, lacking experience other than dabbling a few times in and out of modules. The other developer was our now Chief Technical Officer. He had single-handedly architected a C# system already in use at Universities across the UK. On top of that, he was the sys ops engineer at the time, provisioning and looking after hardware for hosting, amongst other things.</p>
<p>The feeling of pressure was real, not only to do a good job in the eyes of our CEO's who were in the same room at this point but also to my new role model and mentor. There were many times when I would pester him for help, at least that's how it felt. He'd often reply with a clear concise answer mentioning a term or idea I wasn't fully familiar with. Feeling the pressure and anxiety I'd nod, smile and agree as I was too afraid to ask for clarification. "He'll think I'm stupid, not good enough... I'll lose my job". The next 10 minutes would be spent researching the advice he'd given before it left my mind and then trying to implement it.</p>
<p>At some point in our careers, I think we've probably all felt something similar. It could be when joining a new position, or when a new peer joins the team doing a similar role. Maybe you've had a pull request rejected, or an idea shot down by a peer. The anxiety might kick in, feeling you don't belong or that you're not good enough to do your job. You'll be found out, and dragged into a 121 or worse. You'll be <strong>an imposter</strong>.</p>
<p>One of our developers was brave enough to bring up this subject during one of our weekly "tech time" development team meetings; an open forum to discuss tech ideas, team processes or show and tell around something you're proud of. He'd only been with us for maybe 6 months, and although we hadn't worked closely together it came as a bit of a shock. It was a brilliant place to bring it up though, as so many of our peers shared stories of the same feelings in the past when it happened to him and offered reassurance that it was normal to feel that way occasionally. As a team, I feel we've since been more focused on praising and giving a healthy dose of good feedback to peers where possible, such as on code reviews and in team meetings.</p>
<p>On several other occasions since then, I've had members of our development team mention that they get the impression I have everything figured out the same way I believed our CTO had when I first started my career. That was a little surprising to me, as from time to time I still get moments of self-doubt. I'm human, I sometimes make mistakes that make me question if I'm good enough. We've also had new, brilliant highly skilled people join our team doing a similar part of my role which has caused me to make a comparison between myself and them, often leading to similar feelings of self-doubt.</p>
<p>I think what's changed for me is how I try to approach this now. You made a mistake, this is the perfect time to reflect and analyse the root cause. Once you have the root cause, make a plan to avoid it happening again and learn. This might be a team activity or something you do yourself. Mistakes are excellent opportunities to develop and grow, don't waste them. Something generally caused that mistake such as a process that's not quite right that can be adjusted.</p>
<p>The second thing is embracing the opportunity to learn from others. Don't be afraid to ask questions or learn from your peer's wins. An example of this is we've recently had some brilliant agile coaches come to join us. It was amazing to arrange a chat and explain our current process, seek advice from a once outside perspective and develop tangible improvements. These meetings are a regular part of the process where we share the good and the bad helping us all improve.</p>
<p>So next time you suffer from feeling like an imposter, remember some of the peers you're working with have probably felt that way before. They may even be feeling it right now and their reaction to something you suggested is part of a way to defend themselves. Try to focus on learning from your mistakes and soak up the knowledge of your peers, they're both great opportunities to develop. Don't be afraid to ask questions for clarity and don't be afraid to ask for feedback from your peers or line manager. One of our colleagues recently sought feedback from several members of the team as part of their onboarding. This helped completely change their perspective on their progress in a positive way.</p>
<p>For me, as aforementioned, I still get moments of self-doubt from time to time... especially when speaking to our brilliant CTO. The difference is now I'm not afraid to ask questions however stupid I think they might be. Over a decade later and we still work closely together, looking after a team of brilliant, bright, dedicated peers.</p>
]]></content:encoded></item><item><title><![CDATA[Thoughts on fine tuning your tech CV]]></title><description><![CDATA[It's almost cruel that your first step into a potential new role often falls to a few A4 sides attempting to summarise yourself; your skills, your drive…]]></description><link>https://nullabletype.hashnode.dev/thoughts-on-fine-tuning-your-tech-cv</link><guid isPermaLink="true">https://nullabletype.hashnode.dev/thoughts-on-fine-tuning-your-tech-cv</guid><category><![CDATA[Career]]></category><category><![CDATA[interview]]></category><category><![CDATA[recruitment]]></category><category><![CDATA[jobs]]></category><dc:creator><![CDATA[Steven Davies]]></dc:creator><pubDate>Fri, 01 Oct 2021 23:51:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1635551502202/YPi4gkDCF.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It's almost cruel that your first step into a potential new role often falls to a few A4 sides attempting to summarise yourself; your skills, your drive, your goals. You're more than that can cover. The person reviewing your CV knows that, but they face a difficult decision on whether to invest the time to get to know you based on this first introduction.</p>
<p>I've been responsible for hiring into multiple technical positions over the years across different skill areas including developers, testers and sys ops engineers. Some of these have been recent graduates, others looking for senior positions after decades in the industry. </p>
<p>I've seen a wide array of CV styles, some that have delighted me and some that have been difficult to navigate. I thought it may be useful to someone out there, or maybe just myself, to list out some of the suggestions I can think of to help make your tech CV easier to digest. These are all based on real examples I've seen more than once.</p>
<h2 id="consider-your-audience">Consider your audience</h2>
<p>This isn't just for your CV's, this is for everything. Whether you're applying for a new position, putting in a merge request to your peers or writing a best man's speech... it's important to put yourself in the shoes of the person you're writing for. What do they want to know?</p>
<p>Regarding CV's, imagine you're in the position of hiring for the job <strong>you</strong> want. What would you want to know, what information matters and just as importantly what information doesn't?</p>
<h2 id="the-opener">The opener</h2>
<p>Although it is unlikely to be the first section that is read, a personal statement is an opportunity to impress and set a clear indication about what you're looking for and have achieved. If well written, it can easily help you stand out from other candidates.</p>
<p>One of the biggest traps I've seen people fall into is to state the basic cliches. I'm talking about things like "hard-working" or "dedicated", they're really expected traits and don't need to be said. Space on your CV is precious, and I'm sure there are things that you're proud of you want to tell the reader about over basic statements.</p>
<p>The second thing I often see is an overuse of vocabulary. You don't need to impress with fancy words, do so by saying what you've achieved in a clear and easy to read manner.</p>
<p>Pick out the key accomplishments and skills you want the reader to know about you, what the position you're looking for is and the direction of your career. Be honest, you don't want to lie and get an interview for a position you don't want, but feel free to tailor it to the position you're applying to. Keep the fancy words to a minimum, but don't be afraid to be confident about what you're good at. </p>
<p>Here's an example of what I'd consider a good personal statement.</p>
<blockquote>
<p>An <strong>ISTQB certified</strong> software developer in test with <strong>8 years' experience</strong> testing enterprise level web applications. <strong>Designed, written and maintained frameworks</strong> that ensure a <strong>high level of quality and resilience</strong>, including testing web API's and applications for functionality and <strong>performance</strong> that have <strong>reduced required testing time by 50%</strong>. Thrives on the opportunity to <strong>mentor other testers</strong>, work collaboratively in a team and has a keen eye for spotting opportunities for improvement. Now looking for the next challenge to use the skills gained to make a difference and continue professional growth.</p>
</blockquote>
<h2 id="size-matters">Size matters</h2>
<p>If your CV is jam-packed with information covering 10+ pages, then it's going to be difficult to digest and pick out the relevant information you want the reader to see.</p>
<p>Similarly, if your CV is a page or less, you're likely omitting really useful information about yourself and your skills. </p>
<p>The best CV's I've personally seen are generally around 1-3 pages long. Any longer than that then you may benefit from having a tidy.</p>
<h3 id="so-how-do-i-make-the-best-use-of-the-space">So how do I make the best use of the space?</h3>
<h4 id="keep-it-relevant">Keep it relevant</h4>
<p>The IT sector runs at a super-fast pace. Your most recent positions are always likely to be the most relevant on your career path. Take a look at your previous employment. That job you did 5+ years ago using some obscure or out of favour tech... ask yourself, do I want to even do that thing again? Is it relevant to this position or where I want my career to go? </p>
<p>The further back your experience the less specific you probably need to be. Don't omit previous positions altogether, but you can afford more brevity the further back you go. One size doesn't fit all, just use the space wisely.</p>
<h4 id="summary-of-tech-skills">Summary of tech skills</h4>
<p>We use so many tools, so many processes and workflows... you want the person reviewing your CV to see all the wonderful things you know and have used. This can quickly turn into 1/4 of a page of your CV or a giant list of unsorted tools and skills. </p>
<p>The problem is if you're not careful you can really hurt its usefulness. Take a look at the following example:</p>
<blockquote>
<p>C#, Visual studio, MVC, WebApi, Postgresql, MongoDB, T-SQL, Flask, SQLite, LiteDB, Entity Framework, PostgreSQL, SQL Server, AWS, Python, Microsoft Word, Fiddler, Postman...</p>
</blockquote>
<p>The first issue is that there are quite a few things that are not relevant and just distract from what you're actually good at. Things like listing Visual studio along with C# when that's pretty much implied or listing office tools which is an expectation for any IT job. There may also be things you have no real interest in doing again, so think if it's worth listing them. Some may just be completely irrelevant for the position you're applying for.</p>
<p>The second issue is that it doesn't really give any idea of your experience. I've seen lots of CV tip sites that recommend a list of years of experience but that doesn't always help. Take for example C#. Is that building desktop apps, or web APIs? Maybe it's all in windows services or WCF.</p>
<p>My current suggestion would be something like the following, but I'd love to hear/see your ideas 🙂</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Type</td><td>Technologies</td></tr>
</thead>
<tbody>
<tr>
<td>Core Languages</td><td><strong>C#</strong> 7+, Asp.Net 4+ (WebApi &amp; MVC), Entity framework</td></tr>
<tr>
<td>Core Databases</td><td><strong>PostgreSQL</strong>, <strong>MongoDB</strong>, <strong>T-SQL</strong></td></tr>
<tr>
<td>Core Tooling</td><td><strong>OctopusDeploy</strong>, <strong>TeamCity</strong>, <strong>Fiddler</strong>, <strong>Postman</strong></td></tr>
<tr>
<td>Other</td><td><strong>Python</strong>, Flask, <strong>Ms SQL Server</strong>, <strong>SQLite</strong>, <strong>LiteDB</strong>, <strong>AWS</strong> (EC2, X-Ray S3)</td></tr>
</tbody>
</table>
</div><p>It's always a good idea to link any experience you mention in the summary with your job roles and include context, like:</p>
<blockquote>
<p>In this role, I was a <strong>key architect</strong> in developing <strong>large scale</strong> C# (<strong>8.0, .net 4.7.2</strong>) apps with PostgreSQL and entity framework.</p>
</blockquote>
<h2 id="the-design">The design</h2>
<p>This doesn't apply to all roles as it may be useful for a graphic designer position for example but if you're applying for a developer/tester/sys ops type role don't focus too much on the design. Stand out by your clear and obvious thought when preparing your CV by keeping the content clear and easy to read. You don't need colours, graphics or charts to sell yourself, they're just a distraction. I'm not saying make it ugly, just don't overthink it.</p>
<h2 id="the-bonus">The bonus</h2>
<p>You're in this industry (hopefully) because you love tech. You probably have personal projects you work on outside of your normal job. It's always a real bonus to get to see what candidates have worked on in their spare time, throw that GitHub or personal website link in there or in your covering letter.</p>
<p>So that's it, I hope you found some of it useful. Some of the points may seem a little obvious, but they're all based on things I've seen frequently.</p>
<p>I'd love to know if you agree with any of these thoughts, or maybe you have some of your own tips and tricks you'd like to share? ♥</p>
]]></content:encoded></item></channel></rss>