Knowledge gained from Projects

Git-Mastery:

MarkBind:

Open:

RepoSense:

TEAMMATES:

Git-Mastery

DESMOND WONG HUI SHENG

CodeRabbit

CodeRabbit is an AI-powered code review tool that integrates directly into developers' GitHub Pull Requests (PRs). For team project, it acts as an automated "gatekeeper" that catches common mistakes before a human mentor reviews the code.

Learning Points:

  • Learn automated context-aware reviews: CodeRabbit analyzes the entire repository to understand how a change in one file might break another, providing instant, comprehensive feedback on every pull request.
  • Identify edge cases in PRs: It identifies potential issues, such as bugs that static analyzers miss, security oversights, and logic errors that only surface under specific conditions.
  • Summarize complex changes: It automatically generates a summary and walkthrough of code changes, which is particularly handy for large PRs where context helps teammates understand the scope of the work.

Resources:

Claude Agent Skills

Claude Agent Skills is a specialized feature within the Claude ecosystem that allows developers to define and package complex, reusable behaviors as "skills" that the AI can invoke when needed.

Learning Points:

  • Encapsulate Domain Logic: Developers can create specific skills that encapsulate the complex logic of their project, such as a "Scope-Parser-Skill" that handles the different scenario, ensuring the AI uses the most robust method every time.
  • Modularize Intelligence: Instead of overwhelming the AI with a massive list of instructions, developers can break them into modular skills that the agent only activates when the context of the task requires them.
  • Continuous Skill Refinement: Developers can iteratively improve these skills by feeding the agent feedback from failed runs.

Common Usage:
We will create AGENTS.md file. It will contain high-level overview of the repository. Moreover, .claude folder will also be created, it will contains different skill.md files, which contain a more detailed information about the specific component of the repository.
Ideally, when we give a task to AI Agent, it will read through AGENTS.md file to identify the skills needed to perform the task. Thus, it only need to read the relevant skill.md.

Resources:

Git Worktree

Git Worktree is a native Git feature that allows developers to manage multiple working directories (worktrees) attached to a single repository.

Learning Points:

  • Eliminate Context Switching Friction: Instead of using git stash and git checkout to move between branches, developers simply change directories. This preserves their uncommitted changes, terminal history, and IDE state in each branch exactly where developers left them.
  • Optimized Resource Usage: Worktrees share the central .git directory and object database, making them much faster and lighter than a full git clone.
  • Isolated "Agent" Workspaces: In a AI-driven workflow, developers can assign a dedicated AI agent (like Claude Code) to its own worktree. This provides the agent with a bounded context where it can research, build, and test a specific task without interfering with their primary work or other ongoing AI tasks.

Common Usage:
Suppose we are working on a repo named worktree. We can create a new worktree using

git worktree add ../worktree-testing

which create a directory named worktree-testing at the same level as worktree. A new branch worktree-testing, which is the same as directory name, will be created. If we want to specify the branch name, we can use

git worktree add ../worktree-testing -b hotfix

Note that no two worktrees can work on the same branch. Now, we can open worktree-testing separately and make changes accordingly.


If we want to see the full list of worktrees, we can use

git worktree list

Once we are comfortable with the changes, we can just merge or rebase the branch, which is similar to our usual workflow.

git merge hotfix
git rebase hotfix

To delete the worktree, we can remove the worktree-testing directory, and it will be marked as prunable. Then, we can run

git worktree prune

Besides, we can remove clean worktrees (no untracked files and no modification in tracked files) by running

git worktree remove ../worktree-testing 

Resources:

Github Copilot

GitHub Copilot is a sophisticated AI-powered developer tool that functions as an intelligent pair programmer, helping developers write code with greater efficiency and less manual effort. Unlike traditional autocomplete, it uses advanced large language models to understand the deep context of the project to suggest everything from single lines of code to entire functional blocks. By automating routine tasks and providing real-time technical guidance, it allows developers to focus more on high-level problem solving and architectural design.

Learning Points:

  • Ensure Full Repo Understanding: Before implementation, provide Copilot with comprehensive context by indexing the repository and opening relevant files to ensure it understands the project structure and architectural decisions.
  • Verify Understanding via Q&A: Use a Q&A section in Copilot Chat to clarify its internal logic and ensure it isn't making false assumptions about the codebase. Explicitly asking AI not to assume but to verify against actual files (like package.json or custom logic) prevents confident but incorrect "hallucinations".
  • Provide Existing Code Examples: Feed Copilot snippets of the existing project patterns to ensure consistency across the codebase.
  • Demand a Full Implementation Plan: Instruct Copilot to generate a step-by-step reasoning plan before it writes a single line of code. Breaking down complex tasks into atomic, verifiable steps allows developers to identify logical flaws in the AI's approach early on.
  • Evaluate Multiple Solutions: Prompt Copilot to offer several distinct solutions so developers can evaluate the trade-offs in robustness and platform compatibility themselves. Moreover, developers can also provide some possible solution to Github Copilot for evaluation.

Resources:

ChatGPT Codex App

ChatGPT Codex App is an AI coding assistant that helps developers go from idea to working code faster. Instead of only suggesting the next line, it can help with planning, implementation, debugging, and documentation, so it feels more like a practical coding partner in day-to-day work.

Learning Points:

  • Turn plain language into real coding tasks: We can describe the task (debugging, fixing bugs, new implementation, etc) to Codex. Codex can give me a detailed implementation plan before it starting coding. We can exchange idea to better improve our approaches. Once we have reached an agreement, then started coding. Streamline the process, and speed up the development cycle.
  • Use project context for better output: Codex gives much stronger suggestions when it can read the relevant files first, because it can follow the repo's structure, naming style, and existing patterns.
  • Reduce mistakes with quick verification: A good habit is to ask Codex to explain assumptions, point to specific files, and run checks or tests so the final result is safer and more accurate.
  • Speed up debugging work: It can help trace likely root causes, propose focused fixes, and produce small patches that are easier for teammates to review.
  • Write clearer docs faster: Codex is also useful for drafting PR summaries, technical notes, and simple explanations for complex code changes.

Resources:

GitHub Actions

GitHub Actions is a CI/CD automation platform built into GitHub that lets us run workflows directly from our repository. It helps automate repetitive engineering tasks like testing, linting, building, and deployment whenever events such as push, pull request, or manual dispatch happen.

Learning Points:

  • Automate development workflow: We can define workflows in YAML files under .github/workflows to automatically run checks and scripts, which reduces manual work and human error.
  • Protect code quality in pull requests: By running unit tests, lint checks, and security scans on every PR, GitHub Actions helps us catch issues early before merging.
  • Use event-driven pipelines: Workflows can be triggered by repository events (e.g., push, pull_request, release) so automation happens at the right stage of the development cycle.
  • Reuse and scale with actions: We can use community actions from GitHub Marketplace or create our own custom actions to keep workflows modular and maintainable.
  • Secure secrets and deployments: GitHub Actions integrates with encrypted secrets and environment protection rules, allowing safer automation for deployment and external service integration.

Common Usage:
Create a workflow file at .github/workflows/ci.yml and define jobs for testing/building.

An Action is a packaged automation step we can use in a workflow. There are lots of prebuilt Actions made by Github or third parties. We can find these reusable Actions from Marketplace

name: CI

on:
    pull_request:
    push:
        branches: [ main ]

jobs:
    test:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v4
            - name: Setup Node
                uses: actions/setup-node@v4
                with:
                    node-version: 20
            - run: npm ci
            - run: npm test

Jobs run in parallel unless needs is added. Each job has its own steps, and is run in separated virtual environment. Steps inside one job run in order.

${{}} is Github Actions expression syntax. ${{ secrets.USERNAME }} reads an encrypted secret at runtime, and their values are masked in logs. Secrets can be passed through with: or env:.

jobs:
    test:
        runs-on: ${{ matrix.os }}
        strategy:
            matrix:
                os: [ubuntu-latest, macos-latest, windows-latest]
    steps:
        - uses: actions/setup-node@v4
            with: 
                username: ${{ secrets.USERNAME }}
                password: ${{ secrets.password }}
        - run: echo test
    
    deploy:
        needs: test
        runs-on: ubuntu-latest
        steps:
            - run: echo deploy

Resources:

Git Hooks and Lefthook

Git hooks are scripts that Git can run automatically at specific lifecycle events such as pre-commit and pre-push. They help teams enforce quality checks before code is committed or pushed. However, raw Git hooks are stored in .git/hooks (local-only), which makes them hard to version, share, and keep consistent across all contributors.

Lefthook is a fast Git hooks manager that solves this distribution and consistency problem. Instead of manually copying scripts per machine, teams define hooks once in a versioned lefthook.yml, and everyone runs the same checks. This is especially useful for AI-assisted development workflows.

Learning Points:

  • Why we need hooks: Hooks automate repetitive quality tasks (linting, tests, formatting, commit message validation) so issues are caught before they reach CI or code review.
  • What problem raw Git hooks solve (and miss): Native hooks can block bad commits locally, but they are not tracked by default in repository history, so onboarding and team-wide consistency are painful.
  • What Lefthook solves: Lefthook provides a version-controlled, cross-platform, and team-friendly way to define and run hooks with strong performance and parallel execution.
  • Git hooks vs Lefthook: Git hooks are the native trigger mechanism from Git itself; Lefthook is an orchestration layer that manages hook definitions, script execution, and sharing across the team.
  • Why Lefthook is often preferred: It is simple to configure (lefthook.yml), fast on large projects, easy to install in CI/local environments, and reduces "works on my machine" differences in pre-commit behavior.

Common Usage:

  1. Install Lefthook in the project using the package manager that matches your stack.
# Example for a Node.js project
npm install --save-dev lefthook

# Example for macOS via Homebrew
brew install lefthook
  1. Create lefthook.yml in repo root (version-controlled):
pre-commit:
  parallel: true
  commands:
    ruff-lint:
      glob: "*.py"
            run: uv run ruff check --fix {staged_files}
      stage_fixed: true

    ruff-format:
      glob: "*.py"
            run: uv run ruff format {staged_files}
      stage_fixed: true

    mypy:
      glob: "*.py"
      run: uv run mypy .
  1. Run Lefthook install to initialize Git hooks from our Lefthook config.
lefthook install

How it works: lefthook install writes hook launcher scripts into .git/hooks (for events like pre-commit and pre-push). Those launchers then read your versioned lefthook.yml. Each launcher is a small script that calls lefthook run {hook-name} when executed. Usually, we only rerun lefthook install when hook scripts in .git/hooks need regeneration (for example after reinstall, cleanup, or hook wiring changes). Simple command changes in lefthook.yml typically do not require reinstall.

  1. Commit lefthook.yml so all teammates and CI environments share the same hook rules.

Initialize Git Hook Using Git Only:
If we choose not to use Lefthook, we can create native hooks manually.

  1. Create a hook script directly in .git/hooks and make it executable.
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/sh
echo "Running pre-commit checks..."
npm run lint || exit 1
EOF

chmod +x .git/hooks/pre-commit

This works, but the hook is local-only by default and not shared automatically through Git history unless you add extra tooling around it.

Resources:

UV

uv is an extremely fast Python package and project manager. It can replace tools like pip, pip-tools, pipx, poetry, pyenv, virtualenv, and twine in many workflows. It helps with dependency management, virtual environments, Python version management, running scripts, and installing command-line tools.

Learning Points:

  • What uv is used for: uv manages Python projects, dependencies, lockfiles, virtual environments, scripts, and Python versions in one tool.
  • What problem it solves: It reduces the need to combine multiple tools for everyday Python setup and makes project bootstrapping faster and more reproducible.
  • Why it is preferred: uv is much faster than traditional Python packaging tools, uses a global cache, and provides a consistent workflow across macOS, Linux, and Windows.
  • How it improves consistency: uv creates and syncs environments from a lockfile, which helps teams get the same dependencies without manual environment drift.
  • Why it fits modern Python projects: It supports project workflows, scripts, and tools in a way that is simple enough for small projects but scalable for larger codebases.

Common Usage:

  1. Install uv.
# macOS and Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows
winget install --id=astral-sh.uv  -e
  1. Create a new project with uv, or use it in an existing project root.
# New project
uv init example

# Existing project
cd example
uv init
  1. Add dependencies and run commands inside the managed environment.
uv add ruff
uv add --dev pytest
uv run ruff check
uv run python main.py
  1. Keep the environment in sync.
uv lock
uv sync
  1. Use uv to manage Python versions when needed.
uv python install 3.12
uv python pin 3.12

Common Workflow:
For a typical Python project, we usually install uv, run uv init for a new project, add packages with uv add, and use uv run to execute scripts or tools without manually activating a virtual environment.

Resources:

GitFlow and Trunk-Based Development

GitFlow and Trunk-Based Development are two popular Git branching strategies, but they optimize for different team needs.

GitFlow uses multiple long-lived branches (main and develop) plus supporting branches (feature/*, release/*, and hotfix/*). It gives clear release structure and strict separation between ongoing development and production-ready code.

Trunk-Based Development keeps one primary branch (often main) and encourages very short-lived branches or direct commits to trunk, with frequent integration and continuous delivery practices.

Quick Comparison:

Aspect GitFlow Trunk-Based Development
Branch model Multiple long-lived branches like main, develop, release/*, and hotfix/* One main branch, usually main, with very short-lived branches
Release style Scheduled, staged releases Continuous delivery or very frequent releases
Change size Larger batches of work Small, incremental changes
Risk control Release branches and stabilization phases Strong CI, frequent merges, and feature flags
Merge overhead Higher Lower
Best fit Enterprise teams, release-managed products, compliance-heavy workflows SaaS teams, fast-moving product teams, mature CI/CD setups
Main trade-off More process control but more complexity Faster flow but requires discipline and automation

Common Usage:
GitFlow Workflow (release-oriented):

  1. A develop branch is created from main.
  2. A release branch is created from develop.
  3. Feature branches are created from develop.
  4. When a feature is complete it is merged into the develop branch.
  5. When the release branch is done it is merged into develop and main.
  6. If an issue in main is detected, a hotfix branch is created from main.
  7. Once the hotfix is complete it is merged to both develop and main.

Trunk-Based Workflow (continuous integration):

  1. Keep one main integration branch (main).
  2. Use tiny, short-lived branches (hours to 1-2 days max).
  3. Merge to main frequently with passing CI.
  4. Use feature flags to hide incomplete features.
  5. Release continuously or in very small batches.

When to Choose Which:

  • Choose GitFlow when: You need strict release governance, separate QA hardening phases, longer-lived support branches, or compliance-heavy release checkpoints.
  • Choose Trunk-Based Development when: You want rapid delivery, high developer throughput, fast feedback loops, and your CI/CD plus test automation are mature.

Resources:

Semantic Versioned Releases

Semantic versioned releases are a standard way to decide whether a new release should be a major, minor, or patch update. In practice, GitHub does not decide this by itself. A release tool or workflow looks at commits, pull request labels, or version changes, then decides whether a new release should be created.

The most common and practical setup is to use automation on push to main, then let a release tool infer the version bump from commit messages or PR labels.

Learning Points:

  • What this solves: It gives the team a predictable release process instead of manually guessing when to publish a new version.
  • Why it is standard: Semantic versioning makes it easier to communicate compatibility. major means breaking changes, minor means new backward-compatible features, and patch means backward-compatible bug fixes.
  • What GitHub actually does: GitHub Actions can run the release workflow, but the version decision usually comes from tools like semantic-release or release-please.
  • Good practice: Use either commit conventions or PR labels consistently so the automation can infer the right version bump.
  • When it is preferred: This works best for teams that want repeatable releases, clear version history, and CI-driven publishing.

Common Usage:

  1. Choose one version signal strategy and apply it consistently.
Strategy Signal Common labels / messages
Commit-based Conventional commits feat:, fix:, feat!:
PR label-based Pull request labels bump:major, bump:minor, bump:patch
  1. If you use commit-based versioning, merge changes into main with consistent commit messages.
git commit -m "feat: add user search"
git commit -m "fix: handle empty response"
git commit -m "feat!: remove legacy endpoint"
  1. If you use PR label-based versioning, add one bump label before merging.
bump:major  # breaking change
bump:minor  # backward-compatible feature
bump:patch  # backward-compatible fix
  1. Run a GitHub Actions workflow to create releases.
name: Release

on:
    push:
        branches: [main]

jobs:
    release:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v4
            - run: npm ci
            - run: npm test
            - run: npm run build
            - run: npx semantic-release
  1. Let the workflow decide the version bump and create the tag and release.
Commit or label pattern Result
feat:/bump:minor label minor release
fix:/bump:patch label patch release
feat!: or breaking change label/bump:major label major release

When to Choose This:

  • Choose this when: You want release automation, consistent versioning, and a clear mapping between code changes and published versions.
  • Avoid this when: Your team does not follow a consistent commit style or you need fully manual release approval for every version.

GOYAL VIKRAM

GitHub Workflows

GitHub Actions is a workflow automation SaaS which enables the development of CI/CD pipeline through simple YAML code. It was utilized in Git-Mastery to develop our stable and beta release suites, handling building, testing, and publishing.

Furthermore, it is used to run E2E tests to prevent regressions during development.

Key learning points:

  • Preparing the environment and using action triggers in order to enable actions to come together in a sequential workflow.
  • Customizing the machines on which the jobs will be run on to build for multiple operating systems
  • Learning about GitHub secrets to ensure security of workflows

Application Publishing

In the development of the beta publishing workflow, I had to learn how to publish packages to different package managers across different operating systems. In doing so, I learnt:

  • Handling of semvers to allow for automatic updates
  • Setting proper permissions to enable hosting on package manager platforms
  • (Debian specific) Self-hosting packages on GitHub

Each of these aspects varied widely across different OSes. In total, I developed the systems for 4 separate operating systems:

  • Windows
  • MacOS
  • Arch
  • Debian/Ubuntu

Learning the requirements for each system required extensive study, with the documentation being somewhat lacking at times.

Git workflows/Branching Models

Whilst attempting to implement a beta publishing pipeline, I came across multiple different philosophies which our pipeline could follow. As such, I implemented both of these branching models as GitHub workflows. These models are GitFlow and Trunk Based Development. The GitFlow implementation was scrapped in favour of Trunk Based Development.

GitFlow

The central repo holds two primary branches:

  • master
  • development
  1. Feature branches are forked from development and merged back into development when finished
  2. When enough features have been added, a release branch is forked from development. Only bug fixes/documentation can be added to this branch
  3. Once the release is ready to ship, it will be merged back into main and development. The merge into main is tagged with a version number and published.
  4. Hotfixes are done by making a hotfix branch off of main and merging the fix into main and development (main is tagged with an updated release number)
  5. Prone to developers drifting away from each other (in terms of code)

Trunk Based Development

The central repo only hosts a single branch called the "trunk" (known in Git as the main/master branch).

  1. Developers contribute directly to a single branch called the trunk (main or master) and resist making long-lived development branches.
  2. Developers may commit directly to the trunk or use Short-Lived Feature Branches (SLFBs) with PR reviews to contribute
  3. Requires extensive pre-integration testing (compile, unit tests, integration tests) to work, with a robust CI pipeline which checks if the build is broken
  4. Releases occur from the tip of the trunk at certain points in development
  5. Requires branch by abstraction and feature flags, especially in high-throughput teams

JOVAN NG CHENGEN

Knowledge

List the aspects you learned, and the resources you used to learn them, and a brief summary of each resource.


ChatGPT Codex

Resources: Introducing Codex · Codex App · Codex Now GA

Codex is OpenAI's agentic coding system (powered by GPT-5.3-Codex) that can receive a task description, write code, run it in a sandboxed cloud environment, debug errors, and open a pull request for review.

Access & Interfaces:

  • Available via ChatGPT Plus ($30/month), CLI, IDE extension, and the web at chatgpt.com/codex
  • Each task runs in its own isolated cloud sandbox, pre-loaded with your repository

Key Features:

  • Remote Codex: Delegate long-running tasks to cloud machines and open PRs from anywhere — including mobile. Unlike local AI assistants, tasks run asynchronously
  • Worktrees: Run multiple agents in parallel on different features simultaneously, each in isolation
  • Skills: Reusable, shareable instruction sets that align agents with team standards (code understanding, prototyping, documentation)
  • Automations (Background): Codex works unprompted on scheduled workflows — issue triage, alert monitoring, CI/CD integration, and more
  • Mid-task steering: Unlike one-shot prompts, you can guide and interact with Codex while it's working without losing context
  • Integrations: GitHub today; issue trackers and CI systems coming soon

Claude Code

Resources: Claude Code Overview · Claude Code Releases

Claude Code is Anthropic's agentic coding CLI that understands your entire codebase and can work across multiple files and tools to complete tasks end-to-end.

Access & Interfaces:

  • Available via Claude Pro ($20/month) or API
  • CLI (terminal), VS Code extension (inline diffs, @-mentions, plan review), JetBrains plugin, web app at claude.ai/code

Key Features:

  • Multi-agent collaboration: Multiple agents can work together on a task, share context, and hand off work
  • Checkpoints: Saves progress and allows instant rollback to a previous state — critical for long agentic tasks
  • MCP (Model Context Protocol): Open standard for connecting Claude Code to external tools — Google Drive, Jira, Slack, or custom tooling. MCP servers can be self-hosted
  • Git integration: Natively stages changes, writes commit messages, creates branches, and opens PRs
  • Hooks: Shell commands that execute in response to events (tool calls, session start/stop) — enables automated workflows without leaving the harness
  • CLAUDE.md: Project-level instruction files that scope Claude's behavior to your conventions
  • Compared to Codex: Better performance on complex multi-file tasks due to its richer harness; local-first but supports remote agents

Git Hooks

Resources: githooks.com · Lefthook GitHub · pre-commit vs lefthook comparison · Using Lefthook across a team

Git hooks are scripts that run automatically at specific points in the Git workflow. They are stored in .git/hooks/ but are not tracked by version control — hook managers solve this by committing the config to the repo.

Hook Lifecycle:

Hook When Common Uses
pre-commit Before commit is created Lint, format, secret detection
commit-msg After commit message is written Enforce Conventional Commits
pre-push Before push to remote Run tests, check branch policy
post-commit After commit Notifications, doc generation

Tool Comparison — Lefthook vs pre-commit:

Lefthook pre-commit
Language Go Python
Execution Parallel (faster) Sequential
Config lefthook.yml .pre-commit-config.yaml
Hook sources Local scripts Community hook registry
Best for Polyglot/any project Python-heavy projects

Lefthook is preferred in the Git-Mastery project for its performance and language-agnosticism. Configuration example:

# lefthook.yml
pre-commit:
  parallel: true
  commands:
    lint:
      glob: "*.py"
      run: ruff check {staged_files}
    format:
      glob: "*.py"
      run: ruff format {staged_files}

Install with lefthook install after placing lefthook.yml at the repo root.

Git-Mastery implementation: Lefthook was adopted across the exercises repository (#265) and the app repository (#76).


Trunk-Based Development

Resources: Atlassian Guide · DORA Capabilities · trunkbaseddevelopment.com · Harness Complete Guide

A source control branching model where all developers integrate into a single branch (main/trunk), keeping it always releasable and avoiding long-lived branches and merge hell.

Core Principles:

  • Short-lived feature branches only (ideally ≤1 day, ≤1k LoC) — never let branches diverge far from trunk
  • Every commit on main must be in a deployable state — enforce this via CI gate
  • Squash merges for clean history and atomic rollbacks
  • Never merge unless CI passes; require minimum approvals via branch protection rules
  • Feature toggles to ship incomplete features without long-lived branches

Why it works:

  • Forces small, reviewable changes — reduces cognitive load on reviewers
  • Conflict surface shrinks proportionally to integration frequency
  • DORA research confirms TBD is a key predictor of elite software delivery performance (~85% of top-performing teams use CI/CD pipelines)

Minimum Viable CI/CD pipeline:

  1. Unit tests — fast, per-function/module, run on every commit
  2. Integration tests — API-level flows against a virtual environment
  3. E2E / Synthetic tests — run on the actual environment (traditionally flaky; mitigation: retries, stable selectors, isolated state)
  4. Deployment gates — post-deploy smoke tests; rollback automatically on failure

Feature Flags (vs long-lived branches):

  • Incomplete code is merged but hidden behind a runtime toggle
  • Code exists in production but is off — removes the need to maintain a parallel branch
  • Tools: LaunchDarkly, Unleash (open-source), or simple environment variables

CodeQL & Static Analysis

Resources: GitHub CodeQL docs

CodeQL is GitHub's semantic code analysis engine. It treats code as data and runs queries to find security vulnerabilities automatically.

How it works:

  • Creates a database from your codebase by extracting a relational representation of the code
  • Runs queries (written in QL) against that database to find patterns matching known vulnerabilities
  • Results surface as code scanning alerts on PRs and in the Security tab

Key points:

  • Covers OWASP Top 10 out of the box for supported languages (Python, JS/TS, Go, Java, C/C++, etc.)
  • Can be added as a GitHub Actions workflow (github/codeql-action)
  • Free for public repos; included in GitHub Advanced Security for private repos
  • Complements linters (which check style) — CodeQL checks for security and correctness

Git-Mastery implementation: CodeQL was set up alongside main branch hardening (#116).

LOH JIA XIN

GitHub Copilot

GitHub Copilot is an AI-powered coding assistant built by GitHub and OpenAI that helps programmers write code faster and with less effort.

Learning points:

  • Give examples of existing code with similar functionality, to avoid hallucination and ensure the code quality is consistent across the codebase
  • Properly describe the expected usage of the product which is not always obvious when implementing libraries and dependencies. This can affect the implementation method chosen.
  • Ask for many possible solutions (ie. planning) and evaluate them yourself before deciding on one and guiding the AI tool to implement it.

Resources:

Claude Code

Claude Code is an AI coding assistant by Anthropic that helps with code generation, refactoring, debugging, and repository-level understanding through natural language instructions.

Learning points:

  • Give explicit constraints (tech stack, coding style, and file targets) so the tool can make precise edits with fewer iterations.
  • Use task-specific command modes (for example /review) to get focused outputs for different goals such as code review, planning, or implementation.

Resources:

Devin AI

Devin AI is an autonomous AI software engineer by Cognition that can browse the web, write code, and perform multi-step engineering tasks with minimal human intervention.

Learning points:

  • Use Devin for code reviews by pointing it to a specific PR and asking targeted questions, as it can read diffs and provide contextual feedback. It is able to track logic across different files and flag bugs and inconsistencies which involve multiple files, which other AI review agents were unable to flag.

Resources:

SAN MUYUN

ContextManager

A context manager in Python is a structured way to handle resources so setup and cleanup are done safely and automatically, usually through the with statement. Instead of manually opening and closing files or connections, a context manager guarantees that cleanup logic still runs even if an exception occurs inside the block, which reduces bugs and resource leaks. Under the hood, this works through the context manager protocol (__enter__ and __exit__), where __enter__ prepares the resource and __exit__ handles teardown. This pattern makes code cleaner, easier to read, and more reliable because resource handling is localized to one clear block. Common examples include file I/O, database transactions, thread locks, and temporary state changes, and a good rule is to use with whenever something must always be released or restored.

resources:

Utilising Codex for coding

OpenAI Codex is an AI coding agent, a large language model specialized for software tasks, combined with tools that let it work inside a real project. It can be very useful to read and understand files across a repository, run terminal commands, propose and apply code edits, generate test cases etc.

AI Agent:

Agent.md is a project-specific instruction file that tells an AI coding assistant how to behave inside a codebase: what the project does, how the folders are organized, coding conventions, testing commands, and things to avoid. It improves AI performance by informing the LLM exactly how the project’s structure or style from scattered files look like, so it makes more accurate edits, follows the right conventions, avoids breaking assumptions, and produces code that fits the repository better.

Features of a good Agent.md includes:

  • State a clear role
  • Indicates the executable commands
  • Project knowledge
  • Real Examples

Ideally, Agent.md should be very specific on the task that this particular AI Agent should carry out, like writing test cases, debug, refactoring, instead of being very generic.

resources

Prompt Engineering:

Prompt engineering is the practice of designing clear, specific instructions so an AI can produce more accurate, useful, and consistent outputs.

A good prompt should:

  • give enough information about the task
  • provide desired response/output format
  • any constraints that is not clearly stated in the task information provided to the LLM

If the task is complicated, it is always recommended to break it into smaller tasks, and prompt the AI to complete a sequence of smaller tasks.

resources:

GitHub Actions

GitHub Actions is a CI/CD automation tool built into GitHub that lets developers automate tasks directly from a repository. It is commonly used to run tests, build applications, check code quality, and deploy projects whenever certain events happen, such as pushing code or opening a pull request. By defining workflows in YAML files, teams can make their development process more consistent, repeatable, and easier to maintain. Some of the key points as follows:

Workflow automation

GitHub Actions works by defining workflows that are triggered by specific events. For example, a workflow can run whenever someone pushes code, opens a pull request, creates an issue, or on a scheduled time. This reduces the need for developers to manually run repeated commands, because checks and tasks can happen automatically when repository activity occurs. GitHub’s documentation states that workflows can be configured to run on GitHub events, scheduled times, or external events.

CI/CD support

One of the main uses of GitHub Actions is continuous integration and continuous delivery. In continuous integration, code changes are automatically tested before they are merged, helping teams catch bugs earlier. In continuous delivery or deployment, the project can be automatically built and released after the workflow passes. GitHub’s overview specifically mentions using workflows to build and test pull requests, or deploy merged pull requests to production.

YAML-based configuration

Workflows are written as YAML files and are usually stored inside the .github/workflows/ directory of a repository. This means the automation setup is kept together with the project code, making it easier for team members to view, review, and modify the workflow through normal Git version control. GitHub’s workflow syntax documentation explains that a workflow is a configurable automated process defined using a YAML file.

resources:

AI Agent Orchestration

AI Agent Orchestration refers to the coordination of multiple specialized AI agents so that they can work together on complex, multi-step tasks. Instead of relying on one general-purpose AI agent to understand everything, plan everything, write all the code, and check all edge cases, orchestration separates responsibilities across different agents. For example, one agent may act as the orchestrator, another as the planner, another as the coder, and another as the designer. This makes the workflow more structured because each agent focuses on a clearer role while the orchestrator manages task delegation and communication.

Why orchestration is useful

A single AI agent can struggle when the task becomes large, especially in software projects involving backend logic, frontend design, architecture decisions, testing, and documentation. The agent may lose track of context, make inconsistent design choices, or overlook edge cases. By dividing the work among specialized agents, the system can handle complexity more systematically. AWS describes multi-agent orchestration as a way to route tasks or queries to specialized domains while maintaining context across interactions.

Orchestrator agent

The orchestrator is like the coordinator of the whole system. Its role is not necessarily to write code directly, but to understand the user’s high-level request, break it into smaller tasks, and assign those tasks to the correct specialist agents. For example, it may ask a planner to design the implementation approach, then ask a coder to implement the backend, and finally ask a designer to improve the frontend. In your notes, this is described as the orchestrator telling other agents what outcome is needed, rather than telling them exactly how to do their work.

Specialized agents

Different agents can be assigned different responsibilities. A planner agent may inspect the project structure, identify existing patterns, and produce an implementation plan without writing code. A coder agent may focus on writing backend logic, fixing bugs, and implementing functionality. A designer agent may focus on UI/UX, styling, accessibility, and visual consistency. Microsoft’s AutoGen documentation describes agents as self-contained units that can be developed, tested, reused, and combined into more complex systems, which supports this modular approach.

Communication between agents

A key part of orchestration is communication. Agents should not work in isolation, because the planner’s assumptions, the coder’s implementation choices, and the designer’s UI decisions need to stay aligned. Some orchestration patterns use a main agent that coordinates subagents, while others allow agents to hand off control to each other. LangChain’s multi-agent documentation describes both patterns: one where a main agent coordinates subagents as tools, and another where agents transfer control through handoffs.

Planning and validation

Agent orchestration is not only about generating code faster. It can also improve the development process by adding planning and validation stages. For example, the planner can identify edge cases before implementation, while the coder can check whether the implementation follows the plan. This reduces the risk of rushing directly into code without understanding the project structure. LangGraph’s documentation also distinguishes between workflows with predetermined steps and agents that dynamically decide their own process and tool usage, which is useful when designing more controlled agentic systems.

Benefits and limitations

The main advantage of AI orchestration is that it can produce more structured and potentially higher-quality outputs for complex tasks, especially when the agents are given clear roles. However, it also adds complexity. The orchestration setup needs careful prompting, clear agent responsibilities, and good communication rules. It may also increase total token usage because multiple agents are involved, even if each individual agent has a smaller context window. Your notes correctly frame these benefits as something to be further tested rather than as a guaranteed result.

Use Cases:

AI Orchestration should be considered for:

  • Full-stack web application development
  • Large codebase refactoring
  • UI/UX improvement
  • Feature planning and implementation

Should not be used when:

  • You risk running out of tokens
  • Simple or small tasks
  • Project is too small
  • Code explanability is important

resources:

MarkBind

CHUA JIA CHEN THADDAEUS

GitHub Copilot

GitHub Copilot is a code completion and programming AI-assistant that assist users in their IDE. It uses LLMs can assist with code generation, debugging or refactoring through either real-time suggestions or language prompts

Key Learning Points:

  • Context is King! Providing all relevant files immediately leads to faster, more accurate fixes. The AI performs best when it "sees" the full scope of the bug.

  • Don't stop at the first working fix. Specifically, we can try asking the AI to improve code quality and clarity after the initial generation helps eliminate "AI-style" clutter and technical debt.

  • Initial AI suggestions often prioritize the simplest fix. It still requires manually prompts & investigation for edge cases to ensure the solution is robust.

  • We should treat AI as a collaborator, and not an automated system. Reviewing proposed changes and selectively implementing only what fits the project context prevents the introduction of unnecessary or incorrect logic.

Agent Skills

Agent Skills are folders of instructions, scripts, and resources that agents can discover and use to do things more accurately and efficiently. These skills are designed to be automatically founded and used by AI agents, providing them user-specific context they can load on demand hence extending their capabilities based on the task they’re working on.

Key Learning Points:

  • Moving instructions from a giant system prompt into a SKILL.md file allows for "progressive disclosure," where the agent only loads the knowledge it needs for the specific task at hand.

  • Provide a strict directory layout (Instructions in SKILL.md, automation in scripts/, and context in references/) to ensure the agent can reliably find and execute tools.

  • Include specific trigger phrases & tags, as well as clear and well defined steps. This prevents the agent from guessing, thus ensuring it follows the exact logic required for the most effective application.

CJS to ESM migration

One of the major milestone for Markbind this semester was typescript migration and the subsequent CJS to ESM migration. While I only contributed to parts of the process, the opportunity allowed me learn about the details of dependency resolution and the structural differences between module systems, and how the migration to ESM will uture-proof the codebase by enabling better tree-shaking, improved performance, and compatibility with the modern JavaScript ecosystem.

Key Learning Point: Transitioning the CLI and Core packages to TypeScript highlighted the importance of robust type definitions. It transformed the migration from a find-and-replace task into a meaningful refactoring process which in turn helped significantly improve my knowledge of the flow across the codebase.

Full Textual Search with Pagefind

My major contribution to Markbind this semester was contributing to the initial support for full textual search across the site with the use Pagefind.

Before the current Pagefind search, Markbind only supported full textual search across its site by using Algolia's Doc Search plugin which requires an external API key. Otherwise, users would have to the built-in "Heading-Level" search. Hence this feature aims to (eventually) replace the existing built-in search, while providing a full textual search alternative to Algolia's Doc Search plugin

While still far from fully complete, significant progress was made on the feature, most notable of which includes

  1. Flexible filtering options, allowing users to declare their additional specific elements which they would like to exclude from being searchable. This feature is especially helpful for users looking to migrate from Algolia's Doc Search as they can simply declare any existing CSS class used by Algolia.

  2. Integrating page exclusion: Pagefind's indexing now respects the searchable page option declared within site.json.As the goal is to integrate pagefind as the default search within Markbind (eventually replacing the built-in search), this feature will support a seamless onboarding/migration process for user's currently using the default built-in search.

  3. In-House vue component: When initially worked on, Pagefind's supported UI was restrictive and hard to build upon. Hence a major work needed to be redone to overhaul this. Beyond just building a new custom vue component, this task also required custom utility files to help the processing of the pagefind results. While Pagefind has released a new component library improving upon their initial UI, having an In-House vue-component still provides us more customizability as relying on theirs would mean being suseptible to UI changes that we may be less receptive towards.

I thoroughly enjoyed working on the implementation of this feature as it allowed me to tackle the complexities of integrating a third-party indexing tool into a static site generator architecture, while navigating the various additional support to ensure a frictionless experience for users looking to make the switch from existing searches. All in all, it provided me the opportunity to dive deep into codebase of Markbind providing me a better understanding of how various components worked with one-another (especially on the process of site generation).

Key learning point: Full textual search is essential for modern documentation and information retrieval because it bridges the gap between what a user remembers and how a site is structured. While heading-level search relies on the assumption that a page’s title or section headers perfectly encapsulate its entire content, it often excludes the nuanced details, specific code snippets, or technical explanations buried within paragraphs.

Server Driven UI (SDUI)

Along my journey of finding a topic for my upcoming teachback, I stumbled upon a SDUI. As markbind is on web development (, all be it a static site generator), the concept of a SDUI approach towards web development certainly peaked my interest. SDUI is an architectural approach where the backend dictates the user interface's structure, layout, and behavior, rather than hardcoding it in the across different platforms (mobile, web etc).

Key learning points:

  • The backend provides a "view model" (data and structure), while the client act as a renderer for a pre-defined library of native components. Instead of just sending data (like traditional REST APIs), the server sends a response describing how to display the UI (e.g., {"type": "banner", "image": "..."}).
  • As a result, this provides dynamic flexibility as changes to the app's layout, such as rearranging sections on a homepage, can be done immediately by updating the backend, bypassing the need for users to download app updates. Most famously, companies like Airbnb, Lyft and Reddit have made the switch towards using a SDUI approach.

Get Shit Done (GSD)

The topic I eventually decided on for my upcoming teachback was GSD! I chose to do so as (, and putting poop emojis on my presentation was definitely a plus). Additionally, according to its Github repo, its "Trusted by engineers at Amazon, Google, Shopify, and Webflow.", but they don't provide any

The GSD lifecycle can broken down into 3 stages. First begins the initialization stage where you discuss with the agent about what application you aim to build. This helps the agent's research process and its understanding of the various requirements needed by the product. After which it generates a roadmap which structures various phases of implementation. Next comes the iterative process of implementing each phase. In short the user further refining their requirements of the features being implemented for the phase, hence providing greater context for GSD to accurately build initial product discussed.

My experience working with GSD was a surprising positive one (given my friend's previous bad experience working with other AI SDD frameworks). It managed to capture most of what I wanted from the application, providing me with a satisfiable MVP. Having AI build the entire project thus far has certainly been an experience. However, looking back at the project developed, I've come to realise the joy and ownership it takes away from development out of the process.

Key Learning Point:

  • GSD is focused on fast delivery, developing on what is required for each version specified.
  • GSD follows an exploratory process towards development as it plans each of its "phases" one step at a time (instead of doing so right from the beginning like other AI SDD frameworks).
  • As later phases are not pre-planned in detail like BMAD’s, this allows for easier course correction during development as users aren’t locked in from the start.
  • Hence, a major selling point for GSD is the greater flexibility it provides during the development process. This makes it great for any projects where requirements are likely to change in the future, or might require a lot of experimentation within each "phase"

Resources: GH repo

Build More Architect Dreams (BMAD)

BMAD is a structured, multi-agent agile development framework and was often brought up during my research and implementation of GSD. BMAD is perhaps the most opinionated and methodology-heavy implementation of AI-driven development framework.

It is an AI-driven SDLC framework that orchestrates a team of specialized agent personas—such as Product Managers, Architects, and Scrum Masters—through a rigorous, multi-phase pipeline. BMAD aims to enforces a strict sequence from requirements elicitation (PRDs) and technical architecture (ADRs) to implementation, ensuring that AI-generated code is grounded in high-level design rather than isolated prompts.

Key Learning Point:

  • With BMAD, it follows a strict preloaded pipeline and this provides users with a tool that enforces "industrial-grade" software engineering standards automatically.
  • However, a side of which results in all the specs created being heavily interlinked from the start. Hence when if a user is considering to use BMAD to build their software, they must be absolutely sure about what exactly they want to build & as many of its corresponding requirements from the get-go. This is because any changes made mid-way often requires “rewinding” the entire pipeline to re-shard each tasks, while any errors made during each stage can propagate through the entire pipeline.
  • In theory, the BMAD framework provides a highly sophisticated workflow where it aims to minimize technical debt and architectural drift by treating the AI as a disciplined collaborator rather than just a code generator. However, this all comes at the cost of upfront rigidity.

Resources:

Superpowers

Another agentic skills development framework that was often brought up during my research on GSD was Superpowers. It was by far the most popular of the frameworks with over 150k github stars. Notably, it is the only framework in this category officially listed as a plugin on Anthropic’s Claude plugin marketplace which perhaps can be viewed as a form of credibility.

So what is it? Superpowers is a development-centric agentic framework that prioritizes reliability through rigorous automation. It operates by breaking down complex features into granular, testable units, leveraging a loop of automated testing and code refinement.

Key Learning Point:

  • Superpower follows a TDD approach towards implementation and development where it actively writes failing test before writing minimal code for it to pass and then commiting it.
  • Through following a TDD Superpowers aims to provide a high-integrity codebase where all features are verified from the start, significantly reducing the likelihood of regressions or "hallucinated" logic that doesn't actually function.
  • But since the entire codebase is anchored by a specific suite of tests, changing a fundamental requirement mid-stream requires refactoring the tests themselves before the code can be updated.
  • All in all, this creates a high barrier for experimental or "exploratory" coding where the end goal is not yet clearly defined. Hence the frameworks comes at the cost of upfront rigidity making any pivots during the process or after implementation particularly costly.

Resources:

Plan-Apply-Unify Loop (PAUL)

Perhaps the closest sibling to GSD I found whilst researching and implementing GSD. Indeed PAUL was developed on improving the GSD workflow, so much so that they provide documentation on what differentiates it from GSD.

GSD pioneered the concept of treating plans as executable prompts. PAUL takes this further by recognizing that execution without reconciliation creates drift — and drift compounds across sessions.

Where GSD focuses on getting work done, PAUL focuses on getting work done correctly, consistently, and with full traceability.

All in all, PAUL's claims are certainly impressive given my positive experience with GSD thus far. From the videos and forums I've read above PAUL it certainly seems like a promising and ever favourable alternative to GSD. That said, documentation explicitly covering the performance between the 2 are few are far between which somewhat affects its credibility. In comparsion, the current github repo has less than a thousand stars, while GSD has over 50k. I will certainly give it a try over the summer break and look forward to comparing it with GSD for myself.

Key Learning Points:

  • Similar to GSD, PAUL focuses on an iterative approach towards development unlike other AI spec-driven frameworks like BMAD.
  • Interesting differntiator from GSD:
    • Enforced "UNIFY" phase after the execution step to create an audit trail that makes context resumption reliable.
    • Token Economics: Unlike GSD that focuses on the speed of delivery, PAUL focuses on the quality of delivery by reserving subagents use case for parallel research and discovery. This is because while Parallel execution subagents generate more output faster, it is often it's expensive and wasteful. Hence PAUL aims to optimize the value per-token during the execution stage.

Resources:

HARVINDER ARJUN SINGH S/O SUKHWANT SINGH

AI-Assisted Coding Tools

So far, I've experimented with many AI coding tools, incorporating them into my workflow and trying them out.

I've tried opencode, Mistral Vibe, Proxy AI, Github Copilot, Cline.

They can be divided into roughly two categories; AI tools that live in the CLI and those that integrate directly into IDEs. While their usage may be similar (many of them can inject context using slash/@ commands, in addition to other common features), their value can be quite different.

I've found that tools that integrated directly into IDEs felt superior for engineering, provided that one does not enable settings such as "YOLO" mode (editing without permission). This way, you may review the AI's work file-by-file, and guiding it if its approach needs changing.

While I've found human-in-the-loop workflows to feel better as a developer (more supervision over work), less hands-on approaches also can be useful for iterating quickly. However, I've also found that the success of these methods are highly contingent on model quality.

On top of that, leveraging plan/act modes, skills, and keeping context focused can improve model performance.

Resources: Cline documentation

Spec-Driven Development (SDD) Tools

I also explored SDD tools to create a medium-small vibe-coded application. These tools work in a very similar way - they inject skills or workflows (in the form of markdown files) to provide a structured step-by-step approach in conceptualizing and building a whole application based on specification, not code. My experimentations led me to testing out 3 different frameworks: spec-kit, spec-kitty, and BMAD. There is a lot more.

Their target audiences are somewhat different, but the overarching goal is the same: through solely prompting frontier-level models with specification info, create a whole well-coded application. My expectations which would perform better were completely subverted; spec-kit, despite maintained by an industry giant (Microsoft), was not much better than prompting without frameworks. BMAD, highly regarded from my searching and at about 45k stars at time of writing, was by far the most inefficient and poorly-performing. And spec-kitty, an underdog by all rights (1k stars at time of writing), turned out to fit my needs the best and created the best application in a reasonable amount of time.

I don't think these tools are at a level to replace developers wholesale in greenfield projects (they're not really intended for brownfield). But the industry seems to be moving frighteningly fast, and who knows what happens even 1 year from now. But they are indeed useful for creating proof-of-concepts, and can actually be better over framework-less prompting alone.

Overall, here's my learning points:

  • AI Tooling can be deceptive
    • bigger or more popular != better
    • Things that feel like they help LLMs may not actually help them - keeping context windows cleaner may just be better sometimes.
  • Use the right tool
    • A quarter way through my use of BMAD I realized that it was certainly not for small vibe-coded applications. It's intent is a lot more on actual prod-level applications, with a high emphasis on compliance. (This does not change the fact that BMAD still burned a LOT of tokens and failed to produce a working app)

TypeScript (and the JavaScript/Node ecosystem)

So far, I've been working on much of the CLI components of MarkBind. I've done much research on it due to working on PRs such as TypeScript migration and Migrating TypeScript output from CJS to ESM. I would say currently I'm more well-versed in it than an average developer due to my deep involvement in these migration works where I've had to update tooling, workflows, and developer experience regarding TypeScript.

Key Learning Points

  • CJS vs ESM: CommonJS (CJS) and EcmaScript Modules (ESM) are different module systems for the JavaScript environment. Their key difference is in how they resolve imports/other modules - CJS was primarily designed for server-side, and modules are loaded synchronously. This is compared to ESM which allows for both synchronous and asynchronous module loading. As the ecosystem is currently moving towards ESM, MarkBind has migrated to ESM.
  • Node Versions: Even-versioned releases in Node are LTS. Using newer version of Node allow you to use newer features that are sometimes nicer for Dev Experience.
  • Dependency Management: MarkBind, along with much of the current JS/TS ecosystem, uses npm to manage packages. npm uses certain files such as package.json and package-lock.json to keep dependency trees updated and consistent across deployment environments (or just developer PCs). I performed a short presentation on why these exists, how they can get broken, and how to recover when it happens.

Resources: CJS vs ESM (Better Stack)

Intimate Knowledge of the MarkBind codebase

Throughout the semester, I worked across most sections of the MarkBind codebase. I mostly worked on Core and CLI, though in the latter end of the semester I looked at and did some development on the pagefind component in vue-components.

I've learnt how to debug MarkBind when things go wrong, which was useful on February 25th when a change in a transitive dependency (minimatch) silently caused a knock-on effect on MarkBind, resulting in pages not being able to be built and served. Through tedious but well-intentioned tracing, I was able to identify the problematic dependency and temporarily fix it on my end by pinning the dependency's version.

I've also touched on the markbind-action repository, when needing to upgrade node.

Lastly, I learned how to make a new release for MarkBind!

Langchain

I learnt and used Langchain to create an AI workflow for the CSV classifier task.

Resources: Langchain documentation

HON YI HAO

Table of content

AI Coding Tools

Skills

Worked with the team to explore adding repo-specific AI coding skills for common harnesses such as Claude, OpenCode and GitHub Copilot.

Developing AI Skills

Learned about good practices for developing AI skills. Generally, the follow the guidelines provided by claude and study this skill meant to teach AI agents to create skills.

Some notable points include:

Key Principles for AI Skills

  • Do not teach AI general knowledges or concepts that it should already know. It wastes context and may cause confusion, degrading the performance and increasing token usage.
  • Include project/user specific guard rails that the AI agents should follow, i.e.:
    • what NOT to do (e.g. modify files outside of the project directory, work on untracked files, perform database oeprations, etc.)
    • what TO do (e.g. write unit tests, add comments to code, avoid writing trivial comment to code, etc.)
    • HOW to do things (e.g. common make/npm commands to run, lints etc.)
  • Keep the top level SKILL.md concise. Treat it like an index/table of contents, and refer to the other markdown files for detailed instructions and examples.

Most Important

Most importantly: test the skill if they work, and pick up quirks/issues introduced by the skills and tweak your files to improve them. Make sure you run your AI coding tool's debug mode to see if the skills are being picked up.

Meta: Skills in action

This knowledge page and the progress page were formatted using MarkBind skills v0.1.0 with OpenCode using the minimax m2.5 model.

Version controlling AI skills

Why AI skills are useful, they should be treated like code, i.e. it has to be maintained. Otherwise, as project evolves, the code architecture or feature set may change, and the skills would simply confuse the AI agents and make them do wrong things, wasting token and time.

Ways to mitigate skill staleness

There are a few ways to mitigate this:

  1. Use AI to maintain AI
    • Create a subagent that periodically reviews the skill files and updates them based on the current state of the codebase and project requirements.
    • This could be done before PRs are merged, before each releases, or on a regular schedule
    • could be automated using GitHub Actions
    • This way, the skills can evolve with the project without requiring manual updates.
  2. Enforce skill updates in PRs
    • Make it a requirement for developers to review and update the AI skills whenever they make significant changes to the codebase.
    • This can be enforced through code review checklists or automated checks that flag outdated skills based on code changes.

Distributing AI skills

when the skills are not part of your own repo, i.e. meant to be shared with developers outside of your team, you can publish them to a public registry such as a github repository. There are a few ways for user to do this, the most common one being

npx skills install <organization>/<repo>

provided by vercel-lab/skills


Subagents

Created subagents to handle specific tasks such as writing unit tests, generating documentation, and refactoring code.

Useful Tips

  • customizing the subagents to use specific models to balance the cost, speed and intelligence.
  • use the subagents to manage the context window of the main agent.

List of tools experimented with

My current favourite. Open source and supports multiple provider, has strong ecosystem and plugin support. Works well with Neovim with opencode.nvim (the dev is super cool too)


Also works well, especially with Claude's own model. However, the UI is worse than OpenCode and UX is really bad.


Works well and has great VSCode integration. However, rate-limiting became really bad.


Vue

Reactivity (Vue 3)

Learned about Vue 3's reactivity system, including ref, reactive, and computed properties.

Used reactive and computed to implement dynamic data tag count in CardStack Component.

Templates and slots

Learned about Vue's template syntax and slot system for component composition. Useful for creating reusable components with flexible content, especially in the context of MarkBind's custom components.

Other frameworks/meta-frameworks

  • Svelte: explored Svelte's reactivity and component model, which uses a compiler to optimize updates.
  • Astro: learned about Astro's island architecture for building static sites with partial hydration.
  • Next.js: explored Next.js's server-side rendering and API routes for building full-stack React applications.

TypeScript migration

tsc

Learned to use the TypeScript compiler (tsc) to check for type errors and compile TypeScript code to JavaScript.

Useful tsc configs/flags

  • outDir: specifies the output directory for compiled JavaScript files.
  • module: Set to ES2020, ESNext, or CommonJS to control module output format
  • moduleResolution: Use node or node16/nodenext for proper ESM resolution
  • esModuleInterop: Enable to allow default imports from CommonJS modules
  • allowSyntheticDefaultImports: Complement esModuleInterop for synthetic default exports
  • resolveJsonModule: Allow importing JSON files ( useful for package.json reads)
  • declaration: Generate .d.ts type files for library consumers
  • declarationMap: Generate source maps for type files

For pure ESM packages:

  • Add "type": "module" to package.json
  • Use .mjs extension or set "type": "module"
  • tsc --module esnext --outDir dist for ESM output

For dual CJS/ESM packages:

  • Compile two outputs: one for CJS (CommonJS), one for ESM (ESNext)
  • Use package.json exports field with conditional paths: exports: { import: ./dist/esm/index.js, require: ./dist/cjs/index.js }

Sometimes it is necessary to create a tsconfig.json for your LSP to work properly.

PDF generation for MarkBind sites

For those who wants to try it out: https://github.com/yihao03/markbind/tree/feature/export-pdf

When making my own course notes with MarkBind, I realised a need to export the entire site to pdf file so that it could be printed and brought to exams, and distribute easily in general. That's why I started exploring the idea of creating a @/packages/core-pdf module that achieves this goal.

Applications

  • Export MarkBind sites to be distributed as pdf files
    • example use case: generate pdf for CS2103T ip/tp to be submitted for use in CS2101
  • Export MarkBind sites to be printed for offline use
    • example use case: generate pdf for personal notes to be printed and brought to exams

Experimentation

Considering MarkBind already creates a static site with proper formatting, with appropriate CSS for media print, I decided to leverage on that and use a headless browser (Puppeteer) to render the site and print it to pdf.

Challenges

Solved (I think)

Page order when merging
  • In the built _site directory, the pages are individual .html files, with no particular order in the directory.
    • This makes it difficult to determine the correct page order when merging them into a single pdf.
  • Implemented solution:
    • Parse the .html pages to extract the <site-nav> structure, which contains the correct page order.
    • Use the extracted page order to merge the individual pdfs in the correct sequence.

Possible extension:

  • Create a plugin that injexts the prev page and next page links into the pages rather than having to define them manually in each page.
Hidden elementes
  • Some elements (e.g. panels) can be collapsed by default and thus hidden when rendered, which may lead to missing content in the generated pdf.
  • Implemented solution:
    • Inject javascript into the rendered page to expand all collapsible elements before printing to pdf, ensuring all content is included in the final output.

Adding dark mode support to MarkBind sites

I just found out that there's a <mark> component in HTML

"Old school" HTML/CSS/JS

In implementing dark mode, I had to inject a script into page.njk which serves as the template for every page rendered in markbind. To do that, I added a <script> section to read system's theme preference and add a data-bs-theme="dark" attribute to the element where applicable.

CSS scoping with global selectors

When adding dark mode styles inside <style scoped> Vue components, CSS scoping transforms selectors and breaks global attribute selectors like [data-bs-theme="dark"].

Solutions

  • Use :global([data-bs-theme="dark"]) or ::deep() for global ancestor selectors
  • Or move global rules to an unscoped <style> block
  • Bootstrap 5.3 uses data-bs-theme="dark" on <html> for theming

CSS variables for theming

To implement dark mode, I replaced hardcoded color values with bootstrap CSS variables (e.g., var(--bs-body-bg)) that automatically adapt to the theme, ensuring consistent theming across components and maintenability.

Dark Mode Context Overrides

To preserve some custom styles, I had to override some variables in the dark mode context (e.g., [data-bs-theme="dark"]) to ensure the colors look good in dark mode.

Design decisions and trade-offs

Implementing dark mode support introduced a lot of complexity, considering the amount of existing content and styles in MarkBind, especially user defined components and diagrams. Therefore, significant thought has to be put into the design and implementations of the dark mode to ensure minimal friction required for users to adopt it, while also ensuring the implementation is maintainable and does not introduce too much technical debt.

Template value injection into JS (security)

Security: Template Value Injection into JS

Nunjucks template variables embedded directly into JS strings (e.g., const theme = '') can break the script or introduce injection if the value contains quotes or </script>. Must serialize/escape as JSON before injecting template values into JS.

CLI Development

Interactive prompts

@inquirer/prompts provides interactive command-line prompts for CLI tools, useful for setup flows and user identification of harnesses.

Version control for AI skills

Skills pulled from external repos (e.g., MarkBind/skills) can be version-tracked via a .markbind-skills.json file in .agents/skills, similar to package-lock.json. The version is compared against package.json's aiSkillsVersion to prompt users to update.

TypeScript Patterns

CJS require hack in ESM

ESM imports are read-only namespaces, so prototype monkey-patching on imported modules won't work. The workaround is using require('module/path') from node:module to get a mutable CommonJS object while using import type for development-time type safety. This creates technical debt and may break if the package shifts to pure ESM output.

When require is not natively available in ESM, we can use createRequire to create a CommonJS require function:

import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const nunjucks = require('nunjucks');

Open

DALLAS LIM JUIN LOON

AI Coding

OpenCode

OpenCode is a CLI program for agentic coding. It allow users to use the same application with different LLM models, supporting many different providers.

  • Planning: Creating a plan for the agent to follow in order to complete a task allows ensuring implementation will be as specified.
  • Context management: Provide only strictly required information to the agent when prompting and writing project rules to manage the context window effectively. Agent's exploration of the codebase can provide most of the low level details.
  • Skills: Progressive disclosure of skills to the agent allows sterring the agent's behavior only when needed, preventing pollution of context

Gemini code assist

Gemini Code Assist for GitHub is able to review PRs. It was able to find multiple issues in the PRs which i use it in.

  • Security: It found potential atack vectors like decompressing zip files into arbitrary locations
  • Bugs: It found potential bugs like brittle code that could break if string format changes or missing variables when creating hash for cache keys that is supposed to invalidate the cache when changed.

Resources

  • Youtube: Many content creators create videos about agentic coding that can be transferred across different harnesses.
  • Aihero: A website with articles and resources for agentic coding.

QML

A declarative language for designing user interfaces, which can be used through PySide to create GUI applications in python.

Aspects learned

  • Syntax: Using QML classes to create layouts, signals to handle interations, models and views to display data.
  • Resources: Including external resources like images and custom modules to use custom classes written in QML and python.

Resources

Electron

A framework for developing desktop applications using web technologies.

Aspects learned

  • Frontend: Using component library (Maintine) to create user interfaces with react.
  • Architecture: Setting up electron process and allowing the renderer process to communicate with the main process using IPC.
  • Development: Running the electron backend and react frontend without buiding them to watch for code changes and reloading when there are.
  • Packaging: Building both the frontend and backend packaging both into an application for distribution.

GABRIEL MARIO ANTAPUTRA

Git Internals

Learnt about the internal workings of Git, including how Git stores data, how it manages branches, etc.

Electron

Learnt how to develop desktop applications using Electron. In particular, although I had prior experience with Electron, the tech stack was a bit different from what I had used before. For example, I used electron forge before, but this time I used electron builder, which is supposed to be more powerful and flexible. I also had to learn how to set up the project structure and configure the build process for the electron app.

React (Typescript)

Used React with Typescript for the frontend of the electron app. Learnt core concepts like using hooks for state management and side effects, as well as how to structure a React application. I also had to learn how to integrate React with Electron.

Tailwind CSS

Learnt how to use Tailwind CSS for styling the electron app.

Redux

Learnt how to use Redux for state management in the electron app.

Markbind

Learnt how to use Markbind for creating documentation and websites. In particular, I learnt how to setup the markbind project, since I wasn't the one doing that in my CS2103T team.

Github Workflow and CI/CD

Learnt more about Github workflow and CI/CD by setting up Github Actions for my projects. I set up actions for checking linting, building and deploying the pages, building and testing the electron app, etc. The most notable thing I learnt was testing the electron app on mac os environment, which was very useful since I don't have a mac os device to test on.

Prompt and Context Engineering

Practiced context and prompt engineering by comparing self proposed solution with AI generated solution and analyzing the differences. By comparing different prompts and contexts, I learnt how to craft better prompts and optimise context to get better results from AI models.

Using Github Copilot Code Review

Found out about Github Copilot Code Review and used it to review my code and get suggestions for improvements.

RepoSense

CHEN YIZHONG

Tool/Technology 1: GitHub Copilot

  • Collaborative Context Management: Understand how to provide good context for Copilot Agent to work with, such as providing relevant files or documentation snippets (.github/copilot-instructions.md) to helps the agent generate more accurate suggestions.

  • Verification and Human-in-the-Loop: Understand to treat Copilot’s output as a "first draft" that requires additional prompting and extensive testing regarding logic flow and existing architectural constraints.

  • AI-Assisted Code Reviews: Use Copilot directly in GitHub to analyze PRs by asking it to explain complex logic, identify potential edge-case bugs, or ensure adherence to project standards. It can also be called directly to a review on its own.

Resources used:

Tool/Technology 2: Claude Code

  • Agentic Workflows: Experimented with using autonomous subagents capable of executing multi-step tasks like major refactoring. (Breaking down into Planning + Model Making + Writing Tests + Executing with TDD)

  • Efficient Branch Management (Worktrees): Leveraging Git worktrees with Claude Code to perform mutliple concurrent experimental changes in isolated environments, preventing interference with main development tasks.

Resources used:

Tool/Technology 3: renovate Bot

  • Automated Dependency Updates: renovate is a bot that automatically creates pull requests to update project dependencies. It scans dependencies on a configurable schedule, detects available updates, and creates PRs with version bumps—reducing manual effort and keeping projects up-to-date.

  • Group-Based Dependency Configuration: Configured renovate with grouping strategies to batch related dependency updates into single PRs rather than creating one PR per package. This approach reduces notification noise and allows teams to test grouped updates together, improving safety and reducing merge overhead for projects with many dependencies.

Resources used:

Tool/Technology 4: Model Context Protocol (MCP)

  • Understand what is MCP, which is a protocol enabling AI agents to securely access custom tools and resources by acting as an API between AI models and external services.

  • Setting Up Custom MCP Servers: Define tools with schemas, add instructions for AI guidance, implement handlers, and connect via HTTP. Tailor tools and instructions for your specific use cases.

Resources used:

HALDAR ASHMITA

AI Coding Tools

This module exposed me to multiple AI coding tools that are popular in the market, such as GitHub Copilot, Codex by OpenAI, and Claude Code.

Claude Code

My first major use of Claude Code for RepoSense PRs was to implement a YAML configuration wizard, which revealed both the promise and the limits of AI-assisted development.

  • Plan mode proved genuinely useful for scoping out the work ahead.
  • However, I quickly learned that leaving Claude entirely to its own devices was not a viable approach. I had to continuously review the code it produced, prompt it to re-examine its own output for potential issues, and actively probe for bugs — catching problems like a null branch issue that might otherwise have slipped through.
  • Beyond debugging, the collaborative process required real engagement: discussing potential features, weighing multiple implementation approaches, and asking targeted questions. For instance, exploring two different ways to handle git usernames and evaluating the tradeoffs of each.
  • Crucially, I also had to bring my own knowledge of the codebase to the table. When the generated plan referenced CSS files, I was the one who recognised that the main frontend used SCSS and flagged the inconsistency for the sake of consistency, something Claude did not realize until it was told. This is why I kept "manually accept edits" enabled throughout.

The broader takeaway is that working with AI on an existing codebase still demands that the developer be deeply familiar with it. Claude is a powerful collaborator, but it cannot be expected to independently scour a codebase and always arrive at the most contextually appropriate solution. That judgment still has to come from you.

Terminal UIs

While working on my YAML config wizard, I spent some time researching about terminal UIs, when deciding wheher to implement a TUI or GUI for the wizard.

  • A TUI, or Text-based User Interface, is an interactive interface that runs entirely inside a terminal emulator. It is like a GUI but rendered with characters, box-drawing symbols, and ANSI color codes instead of pixels and windows.
  • They have been around since the early days of computing (ncurses-style apps like vim, htop, and midnight commander are classic examples) and have seen a real renaissance with modern frameworks that make them much easier to build.
  • Modern libraries like Bubble Tea (Go's Elm-inspired TUI framework), Rich and Textual for Python, and Ink for React-based terminal rendering can build TUIs.
  • I was weighing whether to go this route, something that lives entirely in the terminal using keyboard navigation, text-based forms, and styled layouts, or go with a more traditional GUI.
  • A TUI felt natural for a config wizard since developers are already in the terminal, it would avoid any frontend dependency overhead, and keeping more of RepoSense CLI-native felt elegant.
  • But ultimately I decided against it. GUIs just offered more flexibility for the kind of user experience I wanted to build, especially for something as structured and potentially complex as YAML configuration, where visual hierarchy, validation feedback, and form layouts benefit from a proper windowed interface.

So after the research detour, I went with a GUI.

.yaml files

A YAML file is a human readable, plain-text file used mainly for configuration. DevOps, and data serialization. It uses indentation-based, hierarchical structures rather than curly braces or bracckets for data storage. Such a key-value pair structure makes it more readable and human friendly compared to JSON or XML. Key aspects:

  • Uses whitespace indentation to define structure, with colons : separating keys and values.
  • It supports multiple data types (such as strings, integers, booleans etc.), sequences such as lists/arrays, and maps.
  • Highly sensitive to indentation (spaces, not tabs). Comments are created with the # symbol.

Worked with YAML files when exploring the development of a GUI .yaml config filee generation wizard.

GitHub Actions

GitHub Actions is GitHub's built-in CI/CD platform that automates workflows in response to repository events like pushes and pull requests.

  • Workflows are defined as YAML files in .github/workflows/ and consist of jobs that run on GitHub-hosted virtual machines (runners).
  • Each job contains a sequence of steps that can run shell commands or use pre-built community actions (e.g., actions/checkout, actions/setup-java).
  • Jobs can run in parallel across a strategy matrix of different OS and tool versions, and workflows support caching, artifact uploads, and conditional execution via if expressions.

Across my contributions to RepoSense, I gained significant practical experience with how GitHub Actions CI interacts with the development workflow.

  • RepoSense's integration.yml runs a build matrix across six OS variants (Ubuntu, macOS, Windows) and includes separate jobs for backend tests (Gradle checkstyleAll, test, systemTest) and Cypress frontend tests.

  • One of my earliest lessons came from the temp repo directory PR (#2537), where I refactored repository cloning to use temporary directories. The change worked locally but broke system tests on CI because the test environment assumed cloned repos persisted across test runs for snapshot reuse. I had to add a SystemUtil.isTestEnvironment() guard to skip cleanup in tests, teaching me that CI runners have different lifecycle assumptions than local development.

    • Similarly, the build.gradle deleteReposAddressDirectory() cleanup function had to be updated to match the new naming convention, showing me how Gradle build scripts and CI are tightly coupled.
  • The title.md to intro.md rename PR (#2516) and its follow-up removal of backward compatibility (#2566) taught me about the CI pipeline's breadth. Changes that touched build.gradle, Vue frontend components, Cypress test support files, and documentation all had to pass linting, checkstyle, unit tests, system tests, and frontend tests across the full OS matrix.

  • The config wizard work gave me the deepest CI experience.

    • I learned that RepoSense's Cypress tests run via Gradle's testFrontend task, which uses the ExecFork plugin to start Vite dev servers as background processes before running tests. When I added wizard-specific Cypress tests, they initially failed in CI because the global support.js beforeEach hook visits localhost:9000 before every spec — interfering with wizard tests on port 9002.
    • I went through a cycle of writing tests, removing them when CI failed, debugging the port isolation issue, fixing it by adding a conditional guard in support.js and using Cypress.env('configWizardBaseUrl') with absolute URLs in intercepts, adding the serveConfigWizard Gradle task with proper dependsOn/mustRunAfter ordering, and then rewriting the tests from scratch.
  • I also encountered multiple rounds of linter failures in CI — Pug lint errors and Java checkstyle violations. That passed locally but were caught by the CI pipeline's stricter enforcement, reinforcing the importance of running the full lint suite locally before pushing.

Calling LLMs in Programs

This activity showed me how to move from using an LLM interactively to embedding it inside a real Python workflow.

  • I learned how to use AI to call an OpenAI model from code using the SDK, pass structured prompts and dataset rows into the model, and capture the output in a machine-usable form for downstream processing. Here, that meant using an LLM to validate keyword labels in a CSV instead of relying only on manual checking.

  • I also learned that calling LLMs in programs requires more than just sending prompts. In practice, we had to handle environment setup, API keys, Python package installation, and compatibility issues between models and SDK parameters.

  • The activity also surfaced operational concerns that matter in scripts: rate limits, batching requests, retries, progress logging, and writing outputs to files like validation_report.csv. I learnt that when LLMs are used programmatically, the surrounding engineering is just as important as the prompt itself.

  • A major takeaway was that LLMs work well in scripts as one component in a larger pipeline. I used deterministic rules for initial labeling, then layered LLM validation on top as a semantic checker. We can combine traditional code for consistency and speed with LLMs for judgment and language understanding.

GitHub Worktrees

Worktrees are a Git feature that allow you to have multiple branches checked out simultaneously on your system, through multiple working directories all associated with a single local repository. This allows users to work on multiple features/branches without constantly switching between them and stashing changes.

  • All worktrees share the same underlying Git history, saving disk space compared to multiple full clones.
  • They eliminate the need for git stash when switching tasks, as each has their own dedicated workspace. Operations like git fetch or creating a new branch are immediately reflected in all linked worktrees.

Commands

  • git worktree list
  • git worktree add <path> [<branch>]
  • git worktree remove <path>
  • git worktree prune

Resources used:

Java Packages

Files, JVM, and Shutdown Hooks

  • Working with the Files package in Java introduced me to how the JVM manages file system interactions at a higher level of abstraction than the older java.io API.
  • Through the java.nio.file package, I explored how to perform common file operations: reading, writing, copying, and deleting, in a more expressive and reliable way.
  • This led naturally into understanding the JVM itself more deeply: how it manages the lifecycle of a running Java program and what happens as it prepares to shut down.
  • From there, I learned about shutdown hooks, which are threads registered with the JVM's runtime that execute automatically when the program is about to terminate, whether through a normal exit or an interruption.
  • This was particularly relevant in the context of file handling when resolving a critical user-reported bug in RepoSense (PR #), as shutdown hooks provide a mechanism to ensure resources are cleanly released and any pending file operations are properly completed before the process exits, guarding against data loss or corruption.

Google Stitch

I presented a teachback on Google Stitch, a Google Labs tool that generates high-fidelity UI designs from plain text prompts, and found it impressive for early-stage prototyping.

  • It runs on an AI-native infinite canvas where you can bring in images, text, or even code as context, and a design agent tracks your progress across the whole project. Under the hood it uses Gemini and Nano Banana.
  • What really sold me on it was the MCP integration with Claude Code — Stitch produces a DESIGN.md file that captures your color tokens, typography scales, and layout rules in a format Claude Code understands natively, so instead of copying and pasting design specs between apps, your agent connects directly and reads them in real time.
  • To wire it up, you need a Google Cloud project with the Stitch API enabled, and then you can either use a Stitch API key (simpler) or OAuth via gcloud (more stable for heavy use)
  • I went with the API key approach first. The Claude Code command is: claude mcp add stitch --transport http https://stitch.googleapis.com/mcp --header "X-Goog-Api-Key: YOUR-API-KEY" -s user, and once that's in you can verify it's live by running /mcp inside Claude Code and checking the server list.
  • The tools Claude Code can then call include create_project, generate_screen_from_text, get_screen_code, and build_site, which means you can describe a screen in natural language, have Stitch generate it, and have Claude Code pull the HTML/CSS and scaffold it into React components, all without ever leaving the terminal.
  • The typical flow for simple apps: five minutes designing in Stitch, a couple minutes for the MCP export, then Claude Code handling the backend and deploying.

Vue.js and Pug

Through building the RepoSense Configuration Wizard, I gained hands-on experience with Vue.js and the Pug templating language.

Vue.js is a progressive JavaScript framework for building user interfaces. It uses a component-based architecture where each .vue file encapsulates a template, script, and style block.

Pug is an indentation-based HTML templating language that eliminates closing tags and angle brackets, producing cleaner and more concise markup. Vue supports Pug natively via <template lang="pug">, allowing developers to write Pug syntax while still using Vue directives like v-model, v-for, and @click.

  • I learned how to structure a Vue project with reusable components, and how to manage cross-component communication through props, events, and a centralized store.
  • Midway through development, I migrated all templates from standard HTML to Pug, which reduced template verbosity significantly.
  • This taught me Pug's indentation-based syntax for expressing HTML hierarchy, how to bind Vue directives in Pug (e.g., v-model, v-for, @click, :class), and the importance of running a Pug linter (puglint) to catch formatting issues.
  • I also learned that Pug's conciseness comes with trade-offs: the indentation sensitivity can make diffs harder to read and requires careful attention to nesting depth, especially in components with complex conditional rendering.

Writing Cypress tests for Vue.js

I wrote a comprehensive Cypress end-to-end test suite for the config wizard.

Cypress is a JavaScript end-to-end testing framework for modern web applications. Unlike Selenium-based tools that run outside the browser, Cypress executes directly in the browser alongside the application, giving it native access to the DOM, network requests, and browser APIs. It provides a chainable command API (e.g., cy.get().type().blur()) for interacting with elements, cy.intercept() for stubbing and spying on network requests, and cy.wait() for synchronizing on asynchronous operations.

  • A key technique I learned was using cy.intercept() to stub backend API calls, which allowed the frontend tests to run independently of the Java backend server.
  • I created reusable setupIntercepts() helper functions that accept overrides, making it easy to test both success and error scenarios. For example, passing { validate: { valid: false, error: 'Repository not found' } } to simulate an invalid repo URL.
  • I also learned how to test multi-step wizard flows by writing navigation helper functions that programmatically advance through earlier steps before testing the target step.
  • Other Cypress patterns I picked up include stubbing window:alert with cy.stub().as('alert') to assert on alert messages, using cy.wait('@alias') to synchronize on intercepted network requests, and configuring a custom baseUrl via Cypress.env('configWizardBaseUrl') so the wizard tests can target a different dev server than the main RepoSense app.

YU LETIAN

Project Knowledge

RepoSense

Vue.js

  • Working with .vue single-file components using Pug templates, scoped SCSS, and Vue 3 composition patterns
  • Built a reusable c-scroll-top-button component with a scroll-container-id prop, avoiding duplication across summary.vue, c-authorship.vue, and c-global-file-browser.vue
  • Used composables as the Vue 3 idiomatic pattern for shared stateful logic

Cypress

  • Ran tests via ./gradlew testFrontend or npm run cypress:open/run
  • Debugged cy.scrollTo() failures — the key lesson: Cypress won't scroll an element unless it's actually scrollable (scrollHeight > clientHeight), so tests need to ensure content is loaded first

AI in Development

Gemini integration for Java

resources Used:

MCP Server Basics

  • Built calculator MCP servers (calculator, plane ticket (with google api), weather reporter) from scratch
  • Understood the architecture
  • Connected local MCP servers to Claude Desktop via claude_desktop_config.json

Setup claude github

  • use claude and copilot to create, review PRs.

TEAMMATES

AARON TOH YINGWEI

Tool/Technology 1

Github Copilot

  • The model used matters. Claude models are much more effective and understanding large code bases and the relationships between different components.
  • Context matters. Conversations that drag for too long tend to hallucinate and give inaccurate answers.

Tool/Technology 2

Claude Code

I familiarised myself with key claude commands that enable me to manage the context window of a conversation and learnt to plug local LLMs into Claude Code.

Context Management

  • Auto-compaction triggers at ~95% usage, summarising older history automatically
  • /compact — manually compress at ~80–90%; run at logical task breakpoints
  • /clear — full wipe; use when switching to an entirely new task

/resume and /rewind

  • /resume — returns to a previous session via an interactive picker; or use claude --resume / claude -c from the terminal
  • /rewind — rolls back to any prior turn; choose to revert conversation only, code only, or both. Trigger with /rewind or double-tap Esc

/model

Switch models mid-session without restarting. Use Option+P / Alt+P as a shortcut while composing.

  • /model opus — most capable, slowest
  • /model sonnet — balanced default
  • /model haiku — fast and lightweight

To use Gemma 4 locally, connect Claude Code to Ollama's Anthropic-compatible API. Gemma 4 (26B MoE, 3.8B active) supports 256K context with zero API cost.

@ File References

  • Type @<filepath> to inject a file's content directly into context. Claude Code autocompletes the path — faster than describing the file or waiting for Claude to find it.

Tool/Technology 3

Upgrading Encryption Stack

  • AES-GCM vs AES-ECB: AES-GCM is an authenticated encryption mode providing both confidentiality and integrity in one primitive, whereas AES-ECB is deterministic and unauthenticated — the same plaintext always produces the same ciphertext, making it vulnerable to pattern analysis.

  • Non-deterministic encryption: AES-GCM uses a random IV per encryption call, so the same plaintext produces a different ciphertext each time. This breaks any code that compares ciphertexts directly and requires a decrypt-then-compare approach instead.

  • SHA-1 vs SHA-256: SHA-1 is considered cryptographically weak by modern standards. Upgrading to SHA-256 provides stronger collision resistance and a larger 256-bit output.

  • Key separation: Using the same key for both AES encryption and HMAC is bad practice as cross-algorithm interactions can theoretically leak key material. Separate independently generated keys should be used for each purpose.

Tool/Technology 4

Schema Management

  • Liquibase change logs - should be the source of truth. Liquibase logs all changes, its authors, time etc and includes rollbacks. Hibernate update is unreviewable and untraceable.
  • Hibernate validate — switch from update to validate so Hibernate verifies the schema matches entities on startup instead of silently patching it. Makes schema drift visible immediately rather than masking missing migrations.
  • Separation of concerns — Liquibase owns the schema; Hibernate only verifies it.

Migration Workflow

  • liquibase-hibernate as referenceUrl — reads the desired schema directly from entity annotations, eliminating the need to spin up a server on a reference branch just to materialise its schema for diffing.

FLORIAN VIOREL TANELY

GitHub Copilot (and agentic coding in general)

Learning points:

  • Provide as much context as possible, using extensions (@), commands (/), and file context (#)
  • Keep each chat focused on a relatively small objective
  • Agent mode is powerful for complex tasks, and it is also a good investigative tool compared with ask mode, where findings are given as a rich document
  • Treat it as a pair programmer; explain and ask questions as you would to a human partner
  • It is easy to get lost while changing code since Copilot makes it much easier to debug and try things out quickly, so it is worth taking the time to ask yourself whether any changes should be part of another PR

Additional learning points (updated at Week 13):

  • Using the CLI version of GitHub Copilot results in a much better experience, as configuring the MCP server and plugins is easier than using the Copilot extensions. Based on my personal experience, it is also more stable than the JetBrains IDE plugin.
  • Compared to Claude, Copilot is more likely to follow our instructions literally.
  • Use the right model (or effort in Claude) for the right tasks.
    • For simpler and isolated tasks, e.g. fixing a bug in a specific React component or resolving syntactic issues, small models like Haiku are sufficient and cost-effective.
    • For more involved tasks, like a bug that depends on the interaction between many components, or developing a relatively isolated new feature, e.g. making words in a lyric clickable to show their dictionary meaning, the standard GPT Codex or Claude Sonnet is enough.
    • For a task that involves a lot of reasoning, like planning a greenfield app or a major refactor of a fundamental service, e.g. migrating the JSON library from Gson to Jackson in a Java app or planning a new authentication method like OIDC, it is better to use the best model possible so that we can minimise back-and-forth and reduce the token usage incurred in fixing any incorrect implementation or plan.
  • A tip I got from one of the mentors, and from my experience vibe-coding an app, is that if we are implementing something big, we should always ask the agent to output its plan, and possibly a list of to-dos or use cases, into a .md file. This allows us to implement each sub-goal in a new conversation by referring the agent to this plan file, minimizing token usage.
  • If we are dealing with libraries that are relatively new, it is worth checking whether they have an MCP for their documentation. We can add this MCP server so that the models will consult the latest up-to-date documentation and minimise hallucinations.
  • Most models have a tendency to over-comment, both in code comments and in user-facing text. For example, when I am vibe-coding an app, Claude often includes implementation-internal details in some user-facing configurations, which could be a security concern and lead to a poor user experience. Adding an instruction against doing this to the agent's base instructions is quite effective in remedying this issue.

Resources used:

  • GitHub website
  • Gemini and ChatGPT

Google Cloud Platform (GCP)

These are some points I learnt when setting up my own GCP project to deploy TEAMMATES to a staging server.

  1. Google App Engine is a service which allows us to deploy a project (backend and frontend) with automatic instance scaling depending on the current traffic. We only need to provision the correct instance type, that is, the maximum resources each instance can use, and choose the one that is most cost-effective for our app.
  2. Services within GCP (App Engine, Cloud SQL, and Compute Engine (VM)) can be set to communicate through an internal network, a service labelled as Virtual Private Connector (VPC). This is safer and perhaps even more efficient compared to wiring up these services to the public network, as the App Engine communicates with the database and the SOLR VM through their local network, which is not exposed to the public internet.
  3. Secrets and environment variables can be configured using the Secret Manager and Parameter Manager services, respectively. This is useful when we want to configure continuous deployment using GitHub Actions.
  4. Proper IAM (Identity and Access Management) is required if we are working as a team on a GCP project. There are numerous roles that can be given to each user and service account, and ideally we should provide each account the minimum required to accomplish a task based on the principle of least privilege for security.
  5. I realised that perhaps profiling with the expected load might give us an idea of which machine to choose, for example, when choosing which DB service tier to provision. Though, perhaps this is dynamic in nature, depending on user-base growth.

OpenID Connect Authentication

OIDC is built on top of the OAuth2 protocol. It is a way for an application (called a relying party / RP) to verify the identity of a user. Before doing so, we need to set up a secret key associated with our client ID and the identity provider. Let's use Google as an example in the following workflow.

  1. When a user clicks "Login", our backend will redirect the browser to the identity provider's login page. The redirect URL will contain our client ID so that the identity provider can identify the RP. This is where the user will log in to their account with the identity provider using their credentials.
    • This involves a consent screen. This basically informs the user what kind of user information the RP can access.
  2. Once the user agrees, the identity provider will redirect the browser back to our backend, specifically to the OAuth2 callback endpoint.
    • OAuth2 callback endpoint: this is an endpoint that the identity provider will redirect the user to after successful authentication. It contains the authorization code, which we can later exchange for an access token and ID token. This authorization code is often embedded as part of the URL query parameter.
  3. Our backend then performs a server-to-server request to the authorization-code exchange endpoint of the identity provider. This request contains the authorization code we get from the callback, our client ID, and client secret. If successful, we will get back an access token. If the protocol supports OIDC, we will also get an ID token.
  • Access token: This token grants the RP access to retrieve user information from the user info endpoint (sometimes also called the resource endpoint) of the identity provider.
  • ID token: This is essentially a key-value pair that contains various "claims", such as sub, indicating the subject, which is the user's ID within the identity provider's system, and iss, which is the URL representing the identity provider. This token is signed with the identity provider's private key, which we can verify using their public key.
  1. OIDC is better than plain OAuth2 as it allows authentication and authorization in one step (step 3). The public key of the identity provider can be downloaded once and used to verify subsequent ID tokens.
  2. The backend can then use these claims about a user's identity to associate it with an account in the system.

Note:

  • To prevent replay attacks, before the backend redirects the browser to the identity provider, it will generate a state, for example, based on the current session_id (a cookie) and next URL. This state will be encrypted. Integrity is ensured by signing the state object using a keyed-hash algorithm or using an encryption algorithm that ensures integrity, such as AES/GCM. The freshness of the session_id protects against replay attacks.

GitHub Actions

  • GitHub Actions is a CI/CD system that allows us to run tasks, such as testing and deployment, upon the occurrence of certain actions such as a push to a pull request or a comment on an issue.
  • We specify a .yaml file to configure parameters such as when the action should be triggered, the name of the action, whether this is a recurring action, and the steps taken. Each action can comprise several steps such as downloading dependencies, compiling the code, and running tests on the code.
  • We can write complicated logic using the official action script plugin. This allows us to use JavaScript to program the actual work being done for a particular step. Using JavaScript improves developer experience too, as inlining the script in a .yaml file or using CLI tools like grep, sed, or echo might not be as readable as a JavaScript file.
  • We can only configure whether to execute several steps sequentially (required if subsequent steps depend on earlier steps, like compiling the code before testing), or run them in parallel if the steps do not depend on one another.

JSON Handling in Java

  • JSON deserialization in Java involves different deserialization methods depending on the choice of library.
  • First, the target deserialization object is constructed. For this step, reflection is used to directly set the values of the object attributes. Other methods include specifying public constructors to use.
  • Then, deserialization is recursively performed for each field. For two entities that are linked to one another, it is important to handle this, usually by using annotations for the JSON library to ignore one side of the relationship in order to prevent an infinite recursion problem.
  • Polymorphic types, in particular, can be handled in several ways. We can write a custom deserializer to check the value of a specific field to know which subtype of a class to construct, or we can use annotations.
    • In Jackson, we can use the @JsonTypeInfo annotation to tell Jackson that a particular field in the serialized entity contains information about the subtype of that entity. The @JsonSubtypes annotation can then be used to map the value of the field specified through @JsonTypeInfo to the subtype information.
    • Ideally, how we design such a subclass relationship impacts how complicated configuring polymorphic relationships can be. For instance, Jackson insists that all subtype entities contain a specific type information field as specified in the superclass, even if we already know what type the serialized entity should be deserialized into. In the case of TEAMMATES, this is solved by overriding the type information annotations in the subclass, but this is certainly not the most elegant solution.
      • Another solution is to always specify the subtype information in the serialized entity.

Unit Testing

A Common Pitfall

This illustrates a mistake I made in testing, which, according to one of my mentors, is a common pitfall.

The point of performing a test on a component (function / method / class) is to confirm that the component is doing what it is supposed to do. This ensures that any future modifications to this component do not break the contract of the component's behaviour. If any such modification is inadvertently made, the test will fail, which means the tests serve their purpose of alerting the developer that something wrong has been done.

In view of this purpose of testing, we should not be too concerned about white-box testing the code to ensure that the code behaves exactly as it is implemented currently. Doing so may distract us from actually ensuring that the requirement / contract is not broken. Also, this requires us to be careful with how we construct the test.

There was this method that I'm testing. Basically, I'm simply testing whether this method would update the internal state of the class properly, given an input. I got too distracted in verifying the actual behaviour of the code (which may not be important in this case), that is, I tried to verify that this method simply sets the internal state using a reference to an object instead of copying the object. This led me to use toBe (which verifies referential equality), which is perfect for this case. However, in this case, I failed to actually test that the component behaves this way. Should someone in the future inadvertently modify the input object in some unexpected way (assuming that this method is simply a setter), the test would still pass since we are comparing the same object!

Simplified Illustration

// Method to be tested
set(o) {
  this.o = o;
}

// Test code
const expectedObject = {};
component.set(expectedObject);
expect(component.get()).toBe(expectedObject);

// Test will pass even if setter is modified (accidentally) to:
set(o) {
  this.o = o;
  o.someProperty = null;
}

The correct way in this case would be to deep-copy the object we provide as input to the component, and then assert that the object we get is still equal by value to the deep-copied object. This way, when someone accidentally changes something in the input object that is not supposed to be done, the test will fail, and it will save the day for everyone!

If we do not deep-copy the object and provide the expected object as the input, even if we compare with toEqual, inadvertent modification to the object will still cause the test to pass, since we are again comparing the same object. We are just checking that all the values from the actual result (the object we get from getting it) are equal to what we provide as input, and both are the same object in memory.

Testing Only What Matters

Another practice worth considering is not comparing all properties of the objects when they may not be necessary for the behaviour we are testing. For example, if a method is only concerned with parsing data into a user-friendly form and storing it in a field, we should not test the equality of other fields within the objects in our assertions.

There are a few reasons against doing otherwise.

1. High Maintenance Burden

When a new field is added in the future, the expected objects would have to be updated for the test to work properly, and this would be costly if we have many expected objects for the purpose of test assertions.

2. Noise in Test Output

When an assertion fails, the output would show all the fields of the objects being asserted as well as all the fields of the actual object, resulting in poor debugging and developer experience. Instead of a failure message stating expected <1> but found <2>, we see something like

expected <{field1: a, field2: b, ...}>
but found <{field1: a, field2: c, ...}>

In addition, it also adds noise in the code itself, in that a test that tests one specific behaviour should not concern itself with irrelevant fields.

KOH WEE JEAN

Lessons I Picked Up

E2E Testing with Selenium & Page Object Model

End-to-end (E2E) testing validates the full user journey through a real browser, and the Page Object Model (POM) is the standard design pattern for keeping such tests maintainable and readable.

Learning points:

  • Page Object Pattern: Encapsulated UI interactions (e.g., waitForElementPresence, button clicks, form fills) into dedicated Page Object classes, decoupling test logic from raw Selenium API calls and making tests easier to maintain.
  • Test Data Management: Structured test data as JSON bundles that map to SQL entities, learning how to convert legacy Datastore fixtures to SQL-compatible formats for use across test suites.
  • Async Synchronization: Handled timing-sensitive interactions such as datepicker widgets and dynamically loaded elements by using explicit waits rather than fixed sleeps, improving test reliability.
  • TestNG Configuration: Registered E2E test suites via testng-e2e-sql.xml configuration files and integrated them into the Gradle build pipeline for consistent CI execution.
  • Bug Identification Through Testing: Discovered and fixed real production bugs (e.g., inverted boolean logic in UpdateFeedbackSessionAction, an overwrite bug in setLogsFromDateTime) as a direct result of writing and running E2E tests.

Resources:

Database Migration (Datastore to SQL)

Large-scale database migrations involve transitioning data models, persistence logic, and test infrastructure from a legacy system to a relational SQL backend without disrupting existing functionality.

Learning points:

  • Liquibase Schema Migrations: Defined and applied incremental database schema changes (e.g., creating the email_templates table) using Liquibase migration scripts, ensuring version-controlled and repeatable schema evolution.
  • JPA Entity Mapping: Adapted legacy Attributes-based Datastore entities to JPA-annotated SQL entities, understanding how annotations like @Entity, @Id, and @Column map Java classes to relational tables.
  • DAO Pattern: Implemented Data Access Objects (e.g., EmailTemplatesDb) to abstract CRUD operations from business logic, keeping the persistence layer cleanly separated and testable.
  • Surrogate Keys & UUIDs: Migrated from natural string-based keys (courseId, session name) to database-generated surrogate keys (UUIDs), and learned how to additively plumb new key parameters through the system during transition.
  • Dual-System Testing: Ran tests against both legacy and SQL backends simultaneously during the migration period, verifying behavioural equivalence and catching divergence early before full cutover.

Resources:

Gradle Build System

Gradle is a flexible build automation tool used in Java projects for compiling, testing, and packaging code, with support for custom task definitions to extend the build pipeline.

Learning points:

  • Custom Task Definition: Created new Gradle tasks (e.g., axeSqlTests) to run specific test suites, learning how to configure source sets, dependencies, and task lifecycle hooks within build.gradle.
  • TestNG Suite Integration: Wired TestNG XML suite files (e.g., testng-axe-sql.xml) into Gradle tasks, enabling targeted execution of accessibility, E2E, and unit test groups independently from the main test run.
  • Build Verification: Used ./gradlew compileTestJava as a fast pre-push check to catch compilation errors early without running the full test suite, improving iteration speed during development.

Resources:

Tools I Picked Up

Gemini Code Assist

Gemini Code Assist is an AI-powered collaborator integrated directly into the IDE to help developers write, understand, and troubleshoot code.

Learning points:

  • Codebase Navigation: Used the assistant to track execution flow and trace logic across multiple files, which is invaluable for understanding how legacy systems and new implementations interact in a large codebase.
  • Linting and Formatting: Leveraged the tool to pre-check code against strict project style guides, quickly catching and resolving checkstyle and linting errors before pushing commits.
  • Context-Aware Debugging: Learned to feed specific error logs and file context into the prompt to rapidly diagnose complex issues, such as database connection failures or syntax errors.

Resources:

Multi-Agent AI Collaboration

Throughout this module, I expanded my workflow to include multiple AI coding assistants, utilizing each agent's specific strengths to optimize the development and review process.

Learning points:

  • Agent Specialization: Discovered how to effectively route tasks based on AI capabilities. I used Gemini LLM primarily for high-level architectural planning, logic structuring, and generating concise summaries of complex tasks, while relying on Claude Code for tasks requiring "deep thinking," intensive coding, and complex refactoring.
  • Meaningful Code Reviews: Learned to leverage GitHub Copilot to conduct insightful and useful code reviews. By feeding the assistant specific pull request context, I could systematically identify edge cases, spot potential logic gaps, and suggest meaningful improvements to peer contributions.

Resources:

PHOEBE YAP XIN HUI

GitHub Copilot

  • Effective for understanding a large codebase using #codebase, helping me to identify relevant directories and files for E2E test migrations

  • Useful for generating repetitive or boilerplate files (e.g. SQL-specific E2E test JSON) when similar examples already exist

  • Less effective at identifying logical errors, often fixing symptoms (modifying test data) instead of root causes (updating test logic)

  • Struggles with browser-based E2E tests due to lack of awareness of actual UI state and rendered content

  • May ignore constraints in prompts and go off-tangent, requiring careful supervision and iteration

  • Different modes can serve different purposes: Ask/Plan for exploration, Edit/Agent for code generation

  • Undo functionality is useful for restarting cleanly.

  • Output quality can be inconsistent even with similar prompts, requiring manual verification (especially for strict JSON formats).

Data Migration

  • Data migration can be approached either top-down (front to back) or bottom-up (back to front), depending on the situation.
    • Example dependency chain: DeleteInstructorActionTest → InstructorLogic → Logic → InstructorsDb.

    • Top-down approach (front to back): Starts from endpoints of dependency

      • Changes are usually non-breaking initially.
      • Risk of missing dependent components if the call chain is not fully traced.
    • Bottom-up approach (back to front): Starts from database or low-level components.

      • Changes are often breaking during migration and require iterative fixes.
      • Immediately reveals all affected files and dependencies.
    • The choice of approach should be made based on the scope, risk, and complexity of the migration task.

GCP in GitHub Actions

Deployment Credentials

  • Treating CI/CD auth as an afterthought is a real risk as deployment credentials have broad cloud access, never expire by default, and are hard to rotate

  • Short-lived tokens (via Workload Identity Federation) eliminate an entire class of risk. If a token leaks, it's expired within the hour

Authentication

  • Workload Identity Federation: Rather than storing a credential, the pipeline proves its identity

    • GitHub issues a signed token containing the repo, branch, and run context, which GCP validates against a trust policy configured once
  • IAM makes least privilege concrete: grant a specific role, to a specific identity, on a specific resource. A compromised deploy pipeline should only be able to deploy

  • Scoping trust by repo and branch goes further because even a fork of the same codebase can't trigger a deploy.

Search Infrastructure

  • Relying on external search engines like Solr increases operational complexity, introduces additional points of failure, and can slow down deployments due to extra configuration and maintenance.

  • Migrating search to PostgreSQL leverages existing infrastructure, reduces maintenance overhead, and streamlines deployment—no more managing a standalone Solr cluster.

Query and Performance

  • PostgreSQL full-text search uses functions like to_tsvector and @@ to match and rank results, enabling fast, relevant search directly within the database layer.

  • Adding proper indexes (such as GIN) is essential as this significantly improves performance and ensures user experience doesn’t regress after migration.

  • Direct SQL-based search lets you fine-tune result ranking and relevance with native SQL expressions, making future tweaks easier for developers.

Security and Configuration

  • With search consolidated in the database, access control and audit trails are centralized, reducing risk compared to cross-system integrations.

  • Old configuration variables, secrets, and maintenance scripts for Solr can be removed for a safer, leaner environment (shrinks the project’s attack surface area).

SARIPALLI BHAGAT SAI REDDY

Cursor

Learned how to use Cursor as an AI-assisted development environment for code generation, refactoring, debugging, and understanding unfamiliar codebases.

Resources used:

  • Cursor official documentation
    Learned the main product features, including inline editing, AI chat, and codebase-aware assistance.
  • Hands-on project experience
    Applied Cursor in real coding tasks, which helped me understand how to use it effectively for implementation and debugging rather than just code generation.

agents.md

Learned how to define project-specific agent instructions using agents.md to improve consistency, task delegation, and adherence to coding conventions.

Resources used:

  • Online examples and community discussions
    Learned how developers use agent instruction files to guide AI behaviour in coding workflows.
  • Personal experimentation
    Practised writing instructions tailored to software engineering tasks such as editing files, following project structure, and maintaining style consistency.

OpenAI API

Learned how to integrate the OpenAI API into software applications to build LLM-powered features and automate language-based tasks, such as csv pasing for form submissions.

Resources used:

  • OpenAI API documentation
    Learned the fundamentals of API usage, request formatting, and response handling.
  • Personal implementation work
    Built familiarity with practical integration concerns such as prompt design, output parsing, and feature prototyping.

Prompt Engineering for Software Engineering Tasks

Learned how to design effective prompts for technical tasks such as code generation, debugging, summarization, and implementation planning.

Resources used:

  • Repeated use of Cursor and OpenAI API
    Learned through experimentation how prompt specificity, constraints, and context improve output quality.
  • Community-shared prompt patterns
    Observed effective prompt structures used by other developers for engineering workflows.

AI-Assisted Debugging and Refactoring

Learned how to use AI tools to analyse bugs, explain code behaviour, and support incremental refactoring in existing systems.

Resources used:

  • Cursor in day-to-day development
    Used AI support to trace bugs, understand unfamiliar code, and explore alternative implementations.
  • Real project experience
    Helped me understand the strengths and limitations of AI when working with non-trivial codebases.

Rapid Prototyping of AI Features

Learned how to quickly prototype AI-enabled product ideas by combining LLM APIs with frontend or backend applications.

Resources used:

  • Personal software projects
    Gained experience turning simple ideas into working prototypes using AI components.
  • OpenAI API documentation
    Provided the technical basis for implementing these features correctly.

RAG Pipeline Design

Learned the fundamentals of RAG pipeline design, especially how retrieval quality, chunking strategy, and context construction affect the usefulness of AI-assisted workflows built on top of existing systems.

Resources used:

  • OpenAI API documentation and general LLM development references
    Helped me understand how retrieval and generation work together in practical applications.
  • Hands-on experimentation
    Built intuition for how document structure and retrieved context affect output quality.

OAuth / OIDC Integration

Learned more about OAuth and OIDC integration, including how authentication and identity claims should be modeled in a provider-agnostic way instead of tightly coupling application logic to one login provider.

Resources used:

  • Real project work on account identity design
    Helped me understand the limitations of provider-specific identifiers and the benefits of more flexible identity modeling.
  • Official references and ecosystem documentation
    Provided context on how provider subject identifiers are used in modern authentication flows.

Schema Evolution and Data Migration

Learned how to approach schema evolution and data migration more safely, especially when refactoring entities, preserving compatibility, and rolling out changes incrementally across a large codebase.

Resources used:

  • TEAMMATES migration-related tasks
    Provided hands-on experience with entity changes, migration scripts, and compatibility concerns.
  • Practical debugging and testing work
    Reinforced the importance of validating migration behaviour carefully before rollout.

Build and CI Tooling

Learned more about build and CI tooling by debugging Gradle and GitHub Actions issues, and by improving automation scripts so they are easier to maintain and extend over time.

Resources used:

  • Hands-on work with Gradle and GitHub Actions
    Helped me understand how build tooling changes can affect developer workflows and CI stability.
  • Official tool documentation
    Used to trace version compatibility issues and update workflow implementations correctly.

Resilient Testing and Environment-Independent Design

Learned the importance of making tests and supporting infrastructure resilient, especially around transaction cleanup, time-sensitive fixtures, and reducing environment-specific assumptions that can cause flaky failures.

Resources used:

  • Integration test debugging
    Showed how small cleanup and fixture issues can cascade into larger test failures.
  • Work on decoupling from GAE-specific behaviour
    Reinforced the value of making systems more portable and predictable across environments.

YONG JUN XI

General Knowledge

OAuth (Open Authorization)

  • OAuth (Open Authorization) is an authorization protocol for third-party apps to have limited access to a user’s protected resources. Usually a request is to be sent to a resource server to obtain the user’s credentials such as email. This works because the resource server already holds my credentials and knows that I am authentic, the third-party app doesn’t need to know my secrets other than the fact that I’m proven to be who I am by the resource server.
    • Step 1 (Pre-callback): Request for an authorization code from the authorization server within a defined scope (e.g. I want the user’s email), the resource server will only be able to serve resources within the scope.
    • Step 2 (Callback): Obtain access token by creating a token request to the authorization server using the authorization code.
    • Step 3: Execute a HTTP request to the resource server API using the access token.
    • Step 4: Extract the resource from the HTTP response and set the login cookie.
  • In simple words, from the client app POV, it only knows the user’s email, but it trusts the user to be authentic because the Google resource server says so (i.e. the user has already logged into Google with their gmail). Hence, the app will allow the user to log in as if they have entered their complete login credentials.
  • OAuth2 for Google API, OAuth2 for Microsoft Entra ID.

MS Entra ID

  • When building a Microsoft client to query the authorization server, we can query the endpoint based on whether we want our app to be single-tenant or multi-tenant.
    • Single-tenant: https://login.microsoft.com/{tenant-id}
    • Multi-tenant: https://login.microsoft.com/common
  • MS Entra recommends using certificates for authentication rather than client secrets. Source

Google Cloud

  • Google Cloud also can be used to monitor the logs in the logs explorer.
  • Go to API & Services -> Click on client ID -> Authorized Redirect URIs to set the redirect URI.

Branding Guidelines

  • Each authentication provider has their own branding guidelines when creating sign in buttons.

Web Servlets

  • Web servlets are mapped to their URL patterns in web.xml, same thing goes to filters such as OriginCheckFilter which is called for every visit to the mapped URL patterns to prevent CSRF attacks.
  • Resource for Jakarta Servlets.

Testing

  • Additional logic in test case may introduce issues:
    • More logic to maintain & might diverge.
    • Depending on the frontend may cause tests to pass if the frontend code is buggy (false-positive).
    • Depending on the local test case external logic may mismatch with what the front-end expects.
  • Be very particular about test cases (we want them to fail to spot bugs).
  • Hard-coded test inputs allow full control of the desired outcome.
  • UUID has a different format than long.
  • The server depends on the docker status, so I need to run docker first.
  • Back door APIs for testing aims to improve testing performance by providing a more direct way to perform API calls without going through the UI, for databases it allows direct manipulation to set up the exact db state the test requires. However, it may introduce security risks like allowing unauthenticated users to make API calls and false positives in test outcomes if front door API calling isn’t tested properly.
  • TestNG: @BeforeTest and @BeforeMethod differences: @BeforeTest runs before the entire test class, @BeforeMethod runs before every @Test in the same class, same applies to @AfterTest and @AfterMethod. Source
  • UAT: The destructive path of error handling would be trying out cases that would make the system accept a faulty input.

Identity Broker vs. Identity Provider

  • Identity broker is like a gateway that sits between the app and identity providers such as Microsoft Entra ID, Google etc.
  • The broker serves the client a list of supported providers and lets the client choose which provider they would like to authenticate with.
  • Once the client chooses, the broker will pass on the authentication request to the respective provider.

Regex

  • /[] defines a set of characters to match within the [].
  • /g is a global flag to match every instance instead of the first one only.
  • Using an escape (denoted using a backslash \), the regex treats the symbol that comes after it literally.
  • E.g. \* would be literally the * character instead of * that represents ‘all’.
  • Same reason to represent a backslash \ literally, it must be written as a double \\.

Word Boundary

  • In regex, a word boundary \b is to match a position between a word character and non-word character.
  • E.g. \\bcat\\b matches cat exactly and not scatter or catholic.
  • This means the character before and after cat must be non-word such as space, punctuations, and asterisks.

Design Patterns

  • Builder Design Pattern to allow separation of construction logic from the final product and enables flexibility in building variations of the same product.

Network Throttling

  • Network throttling is a way for developers to emulate different network speeds by intentionally slowing it down. This feature can be found in the browser developer tools under the network tab.

3NF (Third Normal Form)

  • 3NF (Third Normal Form) is a normalization level in databases to ensure that a non-key attribute doesn’t depend on another non-key attribute.
  • The solution is to encapsulate related attributes into their own table and link the primary key back to the original table as a foreign key.

Tips

  • You can right click and inspect a web UI element to check its ID for debugging.
  • You can terminate the E2E test early to keep a browser open, so when an E2E test fails, I can reload the previous browser to check the latest state of the failed E2E.
  • Splitting a big PR into multiple smaller ones may cause the codebase to be in an non-ideal state (such as removing tests first will introduce a codebase with un-tested features), but fine if done in one go during the same iteration or migration. Other choices include defining the clear boundary of each small PR using commits within the same PR, or push the PRs into a separate branch before merging that branch into master.
  • It’s possible to create a PR to fork’s master branch to run the CI privately.
  • You can change the base branch that a PR merges into if you have write access.
  • We should prioritize reading the DB using more efficient queries.
    • Should avoid making DB queries in a loop.
    • E.g. Fetching all students for a course in one query is better than querying the student for each deadline extension. (1 vs N queries)
  • You can add a --fail-fast flag when running gradlew jobs to force stop it when something fails.
  • TEAMMATES uses HTTP for local development, but authentication requires HTTPS TLS hand shake which causes protocol error during callback.

Tools

Github Copilot

  • Copilot is able to automatically scan similar files and all scripts related to them before writing the json file I need.
  • Non-agentic AI like ChatGPT on browsers can be more useful in catching mistakes in scripts and less prone to hallucinations compared to agentic AI that have to deal with a large context frame.
  • You can ask Copilot to access the git commit history on the current branch to check for potential bugs due to changes made.

VSCode

  • To run the debugger on E2E tests (add –debug-jvm to the gradle command).
  • To print statements when running tests using gradle (add –info to the gradle command).
  • Possible to run “Convert Indentation to Spaces” in the command line in VSCode.
  • Possible to run “Trim Trailing Whitespaces” in the command line in VSCode.

Git

  • Can use keywords like Closes #<issue-number> or Fixes #<issue-number> to automatically close an issue when a PR is merged, but this doesn’t work in issues (an issue cannot close another issue).
  • git switch -c <branch-name> to create and switch to the new branch, which was introduced solely for checking out branches, while ‘git checkout’ can be used for branches, commits, and files.
  • You can create a new branch and make it track an upstream branch using git switch -c <branch-name> <upstream-name>/<upstream-branch-name>.
    • Example: To fetch a branch called chore/new-branch from the upstream repo and work on it locally, simply create a new branch of any name (preferably similar to better distinguish) and make it track that upstream repo branch.
    • Command line: git switch -c chore/new-branch upstream/chore/new-branch.
  • Rebasing with -i (interactive) git rebase -i <target-branch>.
    • Rebasing to move the current commits on top of the HEAD of the target branch (i.e. doesn’t do anything if current branch is already on top).
    • Using interactive tag -i, we are able to edit/refactor the commits on the current branch and rebase will perform the operations.

TestNG

  • Sometimes snapshots can be detected to be obsolete, especially if it doesn’t match the rendering anymore, run ng test --u to update snapshots.

Gradle

  • Gradle runs tests parallelly which is much faster to run everything then a single suite which runs sequentially (Extremely slow).
  • Github CI uses extensive powerful cores that scales dynamically and has fine-tuned resource control, which is why it runs so much faster than locally.

Hibernate

  • Hibernate ignores inserted @UpdateTimestamp annotated attributes.
  • @CreationTimestamp and @UpdateTimestamp are initialized automatically with the same initial value, then @UpdateTimestamp will update itself when the entry is updated. Source

Postman

  • To test an API request, I can use API testing tools like Postman.
  • Making API calls from another origin may require a CSRF (Cross-Site Request Forgery) token, this and the authentication token can be found in any of the HTTP request headers under the network tab.
  • Configure the tokens under Headers tab in Postman.

Apache JMeter

  • Apache JMeter is a tool to simulate user traffic to a website/API endpoint. It can also be used to test HTTP requests using assertions.
  • To configure cookies, go to HTTP Cookie Manager and insert CSRF-TOKEN, AUTH-TOKEN, JSESSIONID as separate entries where applicable.
  • After configuring the managers, you can create a HTTP Request to an endpoint and use assertions to test the response (e.g. Response Code is 200).
  • Running the load testing in CLI is more efficient because it doesn’t have the overhead of rendering the process and results in the GUI, consuming memory and CPU resources. Source for CLI
  • JMeter CLI can generate detailed visualised reports covering response times, throughput, request statistics, request errors etc.
    • Flags for report generation (-e to generate report, -o <empty folder path> to indicate report output folder).

Snyk

  • Snyk is a tool used to monitor/detect security vulnerabilities in package dependencies. Synk CLI can be installed globally (-g) and used to test the project locally.
  • To test for current directory: snyk test
  • Test for all directories: snyk test --all-projects
  • Test for specific file: snyk test --file={file_name}