Skip to content

Software Engineering / Article

Designing and building software with AI without losing professional judgment

Why fast code generation with AI agents does not replace software architecture, technical specifications, and security decisions.

Generating thousands of lines of code in seconds is easy; maintaining, debugging, and taking responsibility for them when things break in production is hard. Using AI agents to write code without a rigorous design process or professional engineering judgment only accelerates technical debt. The difference between a robust system and an automated toy still depends on who understands the real problem, asks the hard questions, detects security risks, and takes ownership of the delivered product.

AI can assist in analysis and speed up execution, but system intent and design remain an exclusively human responsibility.

The short version

  • Intent and judgment are human: AI generates possibilities and runs tasks, but defining the product, its business rules, and security boundaries is a direct engineering responsibility.
  • Strict traceability: Every change must go through a clear, auditable flow: from Jira tickets to a formal feature specification (OpenSpec or similar) and finally to validated code.
  • Isolated context: Do not let agents make decisions blindly. Provide them with project-specific guidelines (AGENTS.md) and critically selected, reusable procedures.

When this helps

This approach of AI-assisted, engineer-led development is indispensable in:

  • Projects with complex business logic where false positives or logical collisions break the service.
  • Teams that need high delivery speed without losing control of system architecture or platform security.
  • Systems with heterogeneous stacks (such as Rust backend and Angular frontend) that require coordinated, atomic changes.

When this does not help

This level of rigor is overkill in:

  • Quick prototypes, disposable mockups, or simple proofs of concept where failures have no operational or reputational cost.
  • Very small personal projects where a single developer has all the context in their head and immediate technical debt does not threaten long-term maintenance.

Practical approach

To illustrate how to structure this assisted flow without losing control, we built a real-world example: Roomly, an enterprise meeting room booking application designed to prevent availability conflicts.

Stack and Repository Structure

Roomly features an Angular frontend, a Rust backend, a PostgreSQL database, JWT authentication with revocable refresh tokens, and tasks managed in Jira. OpenSpec lives at the repository root because it defines the entire system behavior before touching a single line of code:

roomly/
├── openspec/
│   ├── specs/
│   └── changes/
├── backend/
│   ├── AGENTS.md
│   ├── Cargo.toml
│   └── src/
└── frontend/
    ├── AGENTS.md
    ├── angular.json
    └── src/

1. Human Intent and Analysis (ChatGPT Sol)

The product starts from a real operational pain: meeting rooms are coordinated through disjointed messages (emails, chats…), resulting in double bookings. A human professional defines the business intent: “We want a single tool to view and book rooms without conflicts.”

From this direction, we use an analyst agent (ChatGPT Sol) to challenge the idea and surface edge cases: Are consecutive bookings allowed? What are the minimum and maximum durations? How do we handle time zones? The AI proposes options, but the engineer makes the final design decisions: bookings will last between 15 minutes and 8 hours, consecutive bookings are allowed, dates are stored in UTC, and there is no public registration (an admin creates accounts).

Roomly general dashboard Roomly general dashboard.

2. Pragmatic Jira Organization

We translate these decisions into structured Jira tickets. For instance, the epic ROOM-100 (Room Booking) is divided into clean stories like ROOM-102 (Book an Available Room). ChatGPT Sol helps enrich acceptance criteria (authenticated user, required title, reject overlaps, concurrency control), but the engineering team validates that the stories reflect real production needs.

3. From Jira to Executable OpenSpec Contracts

When a ticket moves to development, we create a change folder under openspec/changes/add-room-booking/ containing three pillars:

  • proposal.md: Defines the source (Jira ROOM-102), the scope, and what is explicitly out of scope (e.g., recurring bookings or external calendar integrations).
  • spec.md: Translates user stories into atomic, verifiable behavior scenarios (GIVEN-WHEN-THEN).
    ## Requirement: Prevent incompatible bookings
    
    The system SHALL prevent a room from having two bookings that share the same interval.
    
    ### Scenario: Partial overlap
    - GIVEN a booking exists from 10:00 to 11:00
    - WHEN a new booking is requested from 10:30 to 11:30
    - THEN the system rejects the new booking
  • design.md: Details technical decisions (Rust exposes POST /api/bookings, atomic checks in PostgreSQL, etc.).
  • tasks.md: A task checklist executable by the developer or the agent.

4. First-Use Security and Authentication

Security is not improvised on the fly. We design a secure bootstrap for the first administrator using a one-time console command:

roomly-admin bootstrap --email [email protected]

This command generates a pending account and a temporary token to set the initial password. The refresh token is stored securely and sent in an HttpOnly, Secure, SameSite cookie.

Remember that relying solely on JWT authentication does not guarantee a secure application. A web system has multiple attack vectors, but to keep things focused we will keep this example simple. Cloudy obliges us to remind you that if you have not run a security audit on your software, it likely contains vulnerabilities. If you want to find them before it’s too late, you can contact us for help. And if you have already conducted an audit, a second independent opinion never hurts. Do it for your business’s peace of mind, but above all, to avoid angering Cloudy (she is highly irritable and hates seeing security flaws in production).

5. Execution Context via AGENTS.md and Skills

While the OpenSpec change defines what changes, the AGENTS.md files (one in backend/, one in frontend/) explain how the agent must work in that codebase (Rust conventions, handler/logic separation, backend-enforced authorization, Angular accessibility, etc.). We also select and manually review procedures and scripts from skills.sh before presenting them to the agent.

6. Structured Implementation (ChatGPT Luna)

The development agent (ChatGPT Luna with high effort) receives the complete context: existing code, the OpenSpec change, AGENTS.md guidelines, and approved skills. Instead of inventing its own architecture, the agent generates migrations, endpoints, and Angular components strictly within these boundaries.

7. Code Review and Cross-Validation (Gemini)

We use a second model (Gemini) to perform an automated, independent code review, matching the final diff against the behavior spec and AGENTS.md rules. Gemini identifies potential logical gaps, but a senior engineer makes the final call on what changes to accept. Ultimately, this acts as an extra “just in case” control: with a solid specification, ChatGPT performs exceptionally well, but four eyes see more than two. And the last thing we want is for Cloudy to leave us without pay for another month because of a production slip-up.

8. Final Verification

The last step is manual oversight by us in person. Obviously, we do not read every line of code (that would defeat the productivity benefits of AI), but we do review architectural patterns and critical system components.

Just like building a house: the architect does not inspect every single brick, but they do make sure the foundations are solid, the insulation is placed correctly, and the general construction quality meets expectations.

9. A Complete Flow Example

Let’s assume we are in the middle of implementing a new module and our tickets are ready. The chosen method will succeed as long as it precisely defines all business characteristics and leaves no loose ends. Here, technical experience is everything: this initial step determines project viability, marking the difference between a swift, successful delivery and a hell of endless iterations.

You must deeply understand the project and interpret what the client actually needs. As Cloudy rightly says (though with far coarser words), “what the client asks for is NOT usually what the client actually needs”. You have to look past the initial specification. Cloudy is temperamental, but without her insistence on rigor, we would be little more than lost sheep.

From Jira to the proposal: what we ask explore to do

When ticket ROOM-102 is ready for development, we do not immediately ask the AI to generate code or write a proposal.

First, we initiate an exploration:

/opsx:explore

Next, we provide the agent with the ticket and explicit instructions on what to investigate.

For Roomly, we would use a prompt similar to this:

I want to prepare the implementation of ticket ROOM-102:

“As an authenticated employee, I want to book an available room for a time interval so that I can organize a meeting.”

Acceptance criteria:

- The user must be authenticated.
- They must select a room and specify a title.
- The end date must be after the start date.
- Bookings in the past are not allowed.
- The duration must be between 15 minutes and 8 hours.
- No other overlapping booking can exist.
- Consecutive bookings are allowed.
- In case of two simultaneous requests, only one should succeed.
- The user must receive a confirmation or a clear error message.

Out of scope:

- Recurring bookings.
- Modifying and canceling.
- Invitations.
- Email notifications.
- External calendar integrations.

Before creating any change:

1. Read the AGENTS.md for both backend and frontend.
2. Read openspec/config.yaml and related existing specs.
3. Examine current Rust, PostgreSQL, and Angular code.
4. Check how JWT is validated and how the user context is fetched.
5. Identify affected components, models, endpoints, and views.
6. Look for contradictions, ambiguous requirements, and edge cases.
7. Pay special attention to concurrency, time zones, authorization, validation, and error handling.
8. Check if the ticket conflicts with any existing spec.
9. Propose technical alternatives and explain their trade-offs.
10. Do not implement code or generate change artifacts yet.

Separate the output into:

- Facts confirmed by the ticket or code.
- Necessary decisions.
- Technical alternatives.
- Risks and edge cases.
- Questions that a human must answer.

Do not silently assume any product decisions.

Exploration phase: opencode with openspec processing our requirements ticket Exploration phase: opencode with openspec processing our requirements ticket.

This recommendation to start with the problem and allow the agent to read the code aligns perfectly with the philosophy of explore: investigate before committing to a solution and use what is learned as the foundation for the subsequent proposal.

Expected Exploration Output

After inspecting Roomly, ChatGPT Sol might reply:

Confirmed Facts

- The backend already extracts user_id and role from the JWT.
- Rooms are stored in PostgreSQL.
- Angular already has a service to query rooms.
- A bookings table does not exist yet.
- API dates are represented with time zone information.

Necessary Decisions

1. How we will prevent overlapping bookings.
2. Whether a disabled room can still display future bookings.
3. The maximum length of the meeting title.
4. What happens if a room is deleted/disabled between selection and submission.
5. Whether duration is calculated as real time or local time.

Alternatives for Concurrency

A. Query and create within a database transaction.
   Simple, but safety depends on isolation levels and locks.

B. Use a PostgreSQL exclusion constraint.
   Strong database-level guarantee, though introduces PG-specific syntax.

C. Use an explicit lock per room.
   Explicit, but can increase database contention.

Recommendation

Use a PostgreSQL exclusion constraint as the final guarantee, combined with domain validation in Rust.

Questions for the Team

- What is the maximum title length?
- Can disabled rooms be booked?
- What message should the user see in case of a conflict?
- Can we confirm all intervals are stored as UTC timestamps?

The AI has investigated the problem, but it has not made product decisions on its own.

The Human Intervention

The team reviews the analysis and responds:

Approved Decisions:

- Title length will be between 1 and 200 characters.
- A disabled room cannot receive new bookings.
- Existing bookings for a disabled room remain visible.
- Dates are received with time zone info and stored in UTC.
- Conflict returns HTTP 409.
- User sees: "The room is no longer available for this time slot."
- We will use a PostgreSQL exclusion constraint as the final guarantee.
- Rust will also perform pre-validation to return a readable domain error.

This is one of the most critical parts of the process.

ChatGPT Sol found options and explained their consequences. However, human professionals choose the solution and assume responsibility for it.

Verification Before Proposing

Before creating the change, we make one final request within the same exploration conversation:

Summarize the agreed decisions and check if we have enough information to create the change.

Explicitly state:

- objective;
- scope;
- out of scope;
- affected capabilities;
- main requirements and scenarios;
- technical decisions;
- risks;
- expected tasks.

Do not generate the proposal if any material ambiguity remains.

If the agent detects an unresolved, relevant decision, the team addresses it before moving forward.

When there is sufficient agreement, we instruct:

Convert this exploration into an OpenSpec change called add-room-booking.

Or run directly:

/opsx:propose add-room-booking

The exploration carried out in the conversation becomes the basis of the change. The propose command generates the planning artifacts—proposal, specs, design, and tasks—which the team must read and correct before implementation starts.

Reviewing the Planning Artifacts

Even though the AI generated the documents, the proposal is not yet approved.

The team verifies:

  • that the proposal accurately represents the ticket;
  • that the scope has not crept;
  • that scenarios cover all acceptance criteria;
  • that the design reflects the agreed decisions;
  • that frontend and backend share the same contract;
  • that tasks include tests and migrations;
  • that all new decisions are explicit;
  • that no out-of-scope features were smuggled in.

If we find an issue, we edit the files directly or ask the agent to update the change:

Update add-room-booking with these corrections:

- Maximum title length is 200 characters.
- Add a scenario for disabled rooms.
- Remove the cancellation task; it's out of scope.
- Specify that user_id always comes from the JWT.
- Add a concurrency test with two simultaneous requests.

Keep proposal, specs, design, and tasks consistent.

OpenSpec allows reviewing and modifying artifacts before and during development; the profile also supports update to keep planning consistent when decisions change.

Starting the Apply

Only after human review do we run:

/opsx:apply add-room-booking

ChatGPT Luna then uses:

  • the approved change;
  • the AGENTS.md files;
  • the selected skills;
  • the existing code;
  • decisions made during analysis.

From that moment on, it implements the tasks in tasks.md, tracks its progress, and tests the results against the specification scenarios.

The complete flow looks like this:

ROOM-102 in Jira

/opsx:explore

Code reading and decision detection

Human answers and approval

/opsx:propose add-room-booking

Human review of proposal, specs, design, and tasks

/opsx:apply add-room-booking

Implementation and testing

Gemini review and professional validation

/opsx:archive add-room-booking

Close ROOM-102

Our Rule of Thumb

For small, fully-defined changes, you can run propose directly.

For enterprise-grade features like room booking, we always recommend starting with explore, especially when the change:

  • affects multiple projects;
  • modifies business rules;
  • introduces persistence;
  • involves authentication or permissions;
  • presents concurrency concerns;
  • contains decisions that should not end up hidden in code.

The goal of explore is not to write longer documents.

Its goal is to ensure that, before generating a proposal, someone has asked the right questions and a human has made the important decisions.

Now we repeat the process for all tickets, and in our case, this is the final result:

Roomly general dashboard Roomly general dashboard.

Booking a room in Roomly Booking a room in Roomly.

My bookings list in Roomly My bookings list in Roomly.

A series of database fixtures were created, and the result is quite impressive for a trial run. It meets requirements and performs well—nothing more is asked of it.

However, we must note that we focused purely on functionality. While sufficient for a simple, unexposed application, larger, exposed applications require a significant cybersecurity effort. Remember that an AI does exactly what you ask; if you don’t ask for something, it won’t do it. This principle is a double-edged sword. On one hand, we want it to obey 100% to avoid scope creep or unsolicited features. On the other hand, it won’t consider security unless explicitly told to. This remains the responsibility of the human behind the wheel.

And that human must also understand cybersecurity: how can you ask an agent to be robust against specific vulnerabilities if you don’t even know those vulnerabilities exist? This is why specs must pass through different minds and profiles. In our case, besides development and cybersecurity, we have the most important control: a sheep that lets nothing slip by. It’s not enough to ask the AI to “make the app secure.” You must be explicit. For example, you can add this to your AGENTS.md:

## Security Rule: Identity and Authorization

- Never accept `user_id`, `role`, or permissions from the frontend as a source of authority.
- Retrieve identity exclusively from a validated JWT.
- Validate JWT signature, algorithm, issuer, audience, and expiration.
- Enforce authorization on all protected endpoints.
- The UI can hide actions, but the backend must reject them equally when permissions are lacking.
- Add tests that attempt to execute admin operations with a non-admin user.

Failure modes

  • The illusion of “fast code”: Accepting AI-generated code that seems to work but introduces critical security flaws (such as checking authorization only in the frontend or ignoring database race conditions).
  • Dropping specifications: Relying on the model to remember rules implicitly without documentation. Without a behavioral contract like OpenSpec, codebases quickly drift from original intent.
  • Unvetted dependencies: Blindly importing popular scripts or skills without human verification.
  • Vibe-coding tendencies: Writing “just a bit here, just a bit there” without specs, eventually bloating the codebase outside specifications.

Cloudy check

Cloudy does not smile at autogenerated code that nobody understands. She does not care if ChatGPT Luna generates a thousand lines of Rust if there is no atomic transaction in PostgreSQL to prevent two employees from booking the same room at the exact same second. If you cannot explain line-by-line what your backend does under concurrency, Cloudy will throw your code in the trash. And she will do it with a smirk and a clear “I told you so” expression.

Where this connects

This approach connects directly with our software engineering, AI & automation, and training services. Using AI agents is only safe and cost-effective when the team enforces solid software architecture, secure secrets management, robust authentication, and relational database design.

Where this connects