Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
44fbed9
#17 Improve XML validation: remove delay, update Mustang CLI
novakklemen Oct 13, 2026
4d47d43
#17 Add Copilot instructions for engineering workflow
novakklemen Oct 13, 2026
2026ec3
#17 Add code review checklist and Copilot Chat prompts
novakklemen Oct 13, 2026
d2da814
#17 Document signature support, update README and tests
novakklemen Oct 13, 2026
da78e16
#17 Refactor Mustang CLI jar discovery and versioning
novakklemen Oct 13, 2026
90aa9c7
#17 Add reusable Copilot Chat prompts for repo workflows
novakklemen Oct 14, 2026
cc3fec9
#17 Centralize all repo conventions in AGENTS.md
novakklemen Oct 14, 2026
5f9d291
#17 Add detailed writing style rules for all written content
novakklemen Oct 14, 2026
c2dcf5c
#17 Clarify and update AGENTS.md documentation
novakklemen Oct 14, 2026
421e918
#17 build: add non-blocking format/lint checks, doc branch rules
novakklemen Oct 14, 2026
761ff0b
#17 Add guard script, settings, and docs for tool action rules
novakklemen Oct 14, 2026
938fe65
#17 Add "Maintaining This File" section with guidance
novakklemen Oct 14, 2026
8a404a7
#17 Clarify skill docs, add PR template, update AGENTS.md
novakklemen Oct 14, 2026
8c1d7b1
#17 Warm up Mustang CLI on startup; optimize JVM for fast launch
novakklemen Oct 14, 2026
417b21f
#17 fix: clarify nullability, logging, and validation handling
novakklemen Oct 14, 2026
aa0044e
#17 Add unit tests for FileTypeValidator and TemporaryUploadedFile
novakklemen Oct 14, 2026
1cdd44d
#17 Add unit tests for JavaService and MustangCliService
novakklemen Oct 14, 2026
5c4ef4c
#17 Add comprehensive xUnit v3 tests for REST server and CLI
novakklemen Oct 14, 2026
4587694
#17 Add sample file helpers and integration tests for Factur-X/ZUGFeRD
novakklemen Oct 14, 2026
04eef57
#17 Clarify language param, improve OpenAPI, validate input
novakklemen Oct 14, 2026
a2e7bdc
#17 Normalize line endings and whitespace across repo
novakklemen Oct 14, 2026
fccc91a
#17 docs: add status badges; fix test file formatting
novakklemen Oct 14, 2026
2a32dd8
#17 Fixed Readme to show also GitHub latest release version.
novakklemen Oct 14, 2026
a018c2d
#17 Address review: BOM in .gitattributes, health docs, temp file cle…
Copilot Oct 14, 2026
cd89223
#17 Remove accidentally committed build artifacts
Copilot Oct 14, 2026
bc57c95
#17 Publish coverage badge to 'badges' branch via workflow
novakklemen Oct 14, 2026
466f553
#17 Harden git guard hook, test upload cleanup, run warm-up as hosted…
Copilot Oct 14, 2026
b8d7832
Remove accidentally committed package.g.props with invalid path
Copilot Oct 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions .claude/hooks/guard.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// PreToolUse guard for Claude Code. Reads the hook payload from stdin and exits with code 2
// (which blocks the tool call and shows stderr to the agent) when a protected action is attempted.
// Rules mirror AGENTS.md: no edits to generated files or secrets, no commits or pushes on main,
// no force pushes. Requires only Node.js, which the project already needs for the web UI.
import { execFileSync } from "node:child_process";
import { resolve } from "node:path";

const PROTECTED_FILES = [
{ pattern: /(^|\/)src\/Server\/wwwroot\/assets\//i, reason: "wwwroot/assets is generated build output. Change the sources in src/WebUI and rebuild." },
{ pattern: /\.g\.cs$/i, reason: "*.g.cs files are generated. Change the generator input instead." },
{ pattern: /(^|\/)Mustang-CLI-[^/]*\.jar$/i, reason: "The Mustang CLI JAR is replaced only in a dedicated dependency upgrade (use the upgrade-dependency skill)." },
{ pattern: /(^|\/)\.env(\.[^/]*)?$/i, reason: "Environment files may contain secrets and must not be edited by the agent." },
];

const MAIN_BRANCH = /(?<![\w/-])(?:refs\/heads\/)?main(?![\w-])/;

// Git accepts global options between the executable name and the subcommand.
// They are parsed here so that they cannot be used to hide a commit or push from the checks.
const OPTION_ARGUMENT = String.raw`"[^"]*"|'[^']*'|[^\s"';&|]+`;
const GIT_GLOBAL_OPTIONS = String.raw`(?:\s+(?:-C|-c|--git-dir|--work-tree|--namespace|--exec-path|--super-prefix|--config-env)(?:=|\s+)(?:${OPTION_ARGUMENT})|\s+--[\w-]+|\s+-[pPhv])*`;
const GIT_COMMIT_OR_PUSH = new RegExp(String.raw`\bgit(${GIT_GLOBAL_OPTIONS})\s+(commit|push)\b([^;&|]*)`, "g");
Comment thread
novakklemen marked this conversation as resolved.
const GIT_CHANGE_DIRECTORY = new RegExp(String.raw`(?:^|\s)-C\s+(${OPTION_ARGUMENT})`, "g");

function block(message) {
process.stderr.write(`Blocked by .claude/hooks/guard.mjs: ${message}\n`);
process.exit(2);
}

function currentBranch(cwd) {
try {
return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd, encoding: "utf8" }).trim();
} catch {
return "";
}
}

function checkFileEdit(toolInput) {
const rawPath = toolInput?.file_path ?? toolInput?.notebook_path ?? "";
const path = String(rawPath).replaceAll("\\", "/");
const match = PROTECTED_FILES.find((rule) => rule.pattern.test(path));
if (match) {
block(`${path}: ${match.reason}`);
}
}

function unquote(value) {
return /^(".*"|'.*')$/.test(value) ? value.slice(1, -1) : value;
}

function effectiveDirectory(globalOptions, cwd) {
let directory = cwd || process.cwd();
for (const match of globalOptions.matchAll(GIT_CHANGE_DIRECTORY)) {
directory = resolve(directory, unquote(match[1]));
}
return directory;
}

function checkShellCommand(command, cwd) {
for (const [, globalOptions, subcommand, argumentsText] of command.matchAll(GIT_COMMIT_OR_PUSH)) {
if (subcommand === "push") {
if (/(\s--force\b|\s--force-with-lease\b|\s-f\b)/.test(argumentsText)) {
Comment thread
novakklemen marked this conversation as resolved.
block("force pushes are not allowed.");
}
if (MAIN_BRANCH.test(argumentsText)) {
block("pushing to main is not allowed. Push a feature branch and open a pull request.");
}
}

if (currentBranch(effectiveDirectory(globalOptions, cwd)) === "main") {
block(`the current branch is main. Create a branch first (see Branching Strategy in AGENTS.md).`);
}
}
}

let raw = "";
for await (const chunk of process.stdin) {
raw += chunk;
}

let payload;
try {
payload = JSON.parse(raw);
} catch {
process.exit(0);
}

const toolName = payload.tool_name ?? "";
const toolInput = payload.tool_input ?? {};

if (["Edit", "Write", "MultiEdit", "NotebookEdit"].includes(toolName)) {
checkFileEdit(toolInput);
} else if (toolName === "Bash") {
checkShellCommand(String(toolInput.command ?? ""), payload.cwd);
Comment thread
novakklemen marked this conversation as resolved.
}

process.exit(0);
78 changes: 78 additions & 0 deletions .claude/hooks/guard.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Regression tests for guard.mjs. Run with: node --test .claude/hooks/guard.test.mjs
import assert from "node:assert/strict";
import { execFileSync, spawnSync } from "node:child_process";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { after, before, describe, it } from "node:test";
import { fileURLToPath } from "node:url";

const guardPath = fileURLToPath(new URL("./guard.mjs", import.meta.url));

function runGuard(toolName, toolInput, cwd) {
const result = spawnSync("node", [guardPath], {
input: JSON.stringify({ tool_name: toolName, tool_input: toolInput, cwd }),
encoding: "utf8",
});
return result.status;
}

function runBash(command, cwd) {
return runGuard("Bash", { command }, cwd);
}

describe("guard.mjs", () => {
let mainRepo;
let featureRepo;

before(() => {
mainRepo = mkdtempSync(join(tmpdir(), "guard-main-"));
featureRepo = mkdtempSync(join(tmpdir(), "guard-feature-"));
for (const [repo, branch] of [[mainRepo, "main"], [featureRepo, "feature/1_test"]]) {
execFileSync("git", ["init", `--initial-branch=${branch}`], { cwd: repo });
// A branch name is only reported once the repository has a first commit.
execFileSync("git", ["-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "--allow-empty", "-m", "init"], { cwd: repo });
}
});

after(() => {
rmSync(mainRepo, { recursive: true, force: true });
rmSync(featureRepo, { recursive: true, force: true });
});

const blockedFromFeatureBranch = [
"git push --force origin feature/1_test",
"git -C /some/repo push --force origin feature/1_test",
"git -c core.editor=true push -f origin feature/1_test",
"git --no-pager push origin main",
"git -C /some/repo push origin main",
"git push origin HEAD:refs/heads/main",
"git --git-dir=.git push --force-with-lease origin feature/1_test",
];

for (const command of blockedFromFeatureBranch) {
it(`blocks "${command}"`, () => {
assert.equal(runBash(command, featureRepo), 2);
});
}

it("allows a normal push of a feature branch", () => {
assert.equal(runBash("git push origin feature/1_test", featureRepo), 0);
});

it("blocks a commit on main", () => {
assert.equal(runBash("git commit -m test", mainRepo), 2);
});

it("blocks a commit on main when git -C points to the main repository", () => {
assert.equal(runBash(`git -C "${mainRepo}" commit -m test`, featureRepo), 2);
});

it("allows a commit on a feature branch when git -C points to it", () => {
assert.equal(runBash(`git -C ${featureRepo} commit -m test`, mainRepo), 0);
});

it("blocks edits to generated files", () => {
assert.equal(runGuard("Edit", { file_path: "src/Server/wwwroot/assets/index.js" }, featureRepo), 2);
});
});
62 changes: 62 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Bash(dotnet restore *)",
"Bash(dotnet build *)",
"Bash(dotnet test *)",
"Bash(dotnet format *)",
"Bash(dotnet list *)",
"Bash(npm ci)",
"Bash(npm run build)",
"Bash(npm run lint)",
"Bash(npm audit *)",
"Bash(npm outdated)",
"Bash(git status *)",
"Bash(git diff *)",
"Bash(git log *)",
"Bash(git show *)",
"Bash(git branch *)",
"Bash(git fetch *)"
],
"ask": [
"Bash(git add *)",
"Bash(git commit *)",
"Bash(git push *)",
"Bash(git checkout *)",
"Bash(git switch *)",
"Bash(git rebase *)",
"Bash(git merge *)",
"Bash(gh pr create *)",
"Bash(gh pr merge *)",
"Bash(npm install *)",
"Bash(npm audit fix *)"
],
"deny": [
"Bash(git push --force *)",
"Bash(git push -f *)",
"Bash(git reset --hard *)",
"Bash(git clean *)",
"Bash(rm -rf *)",
"Bash(Remove-Item * -Recurse *)",
"Read(./.env)",
"Read(./.env.*)",
"Read(./**/secrets.*)",
"Edit(./src/Server/wwwroot/assets/**)",
"Edit(./**/*.g.cs)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|MultiEdit|NotebookEdit|Bash",
"hooks": [
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard.mjs\""
}
]
}
]
}
}
14 changes: 14 additions & 0 deletions .claude/skills/add-tests/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
name: add-tests
description: Write xUnit v3 tests for the selected code in src/Server.Tests. Use when asked to add or extend unit tests.
---
Write tests for the selected code in `src/Server.Tests`, following the "Tests" section of `AGENTS.md`.

- Mirror the source structure and naming (`<Type>Tests`).
- xUnit v3 with `[Fact]` / `[Theory]`, built-in `Assert`, Arrange-Act-Assert.
- Cover normal, edge (empty/invalid input), cancellation and failure cases.
- Test via public APIs only; mock only external dependencies (Java process, file system).
- No loops or conditionals; deterministic and parallel-safe.
- Document every public test class and test method with XML docs (`<summary>`, plus `<param>` for theory and fixture parameters).

Run `dotnet test src/Docentric.EInvoice.Validator.slnx` and fix failures.
12 changes: 12 additions & 0 deletions .claude/skills/commit-message/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
name: commit-message
description: Write a commit message in the repository format for the current staged changes (or working tree). Use when the user asks for a commit message. Only returns text and does not commit.
---
Write a commit message for the current staged changes (or the working tree if nothing is staged).

- Format: `#<issue> <Imperative summary>`; infer the issue number from the branch name, otherwise use `#noissue`.
- Capitalized imperative summary, no trailing period, about 72 characters or less.
- Add a body only if the motivation, trade-offs or breaking changes are not obvious.
- If the changes mix unrelated concerns, say so and suggest how to split them.

Return only the message.
15 changes: 15 additions & 0 deletions .claude/skills/create-branch-implement-and-pr/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
name: create-branch-implement-and-pr
description: Run the full issue workflow end to end. Creates the branch, implements, reviews, asks the user to confirm, then opens the pull request. Use only when the user wants the whole flow. For a single step use create-branch, implement, review-implementation or create-pull-request.
argument-hint: "[issue number]"
disable-model-invocation: true
---
Work on the GitHub issue given as the argument (ask for the number if missing). Run these skills in order and follow their instructions exactly. Do not repeat their steps here.

1. `create-branch`: stop and report if a branch or PR already exists for the issue.
2. `implement`: implement, build and test. Skip its closing question, because the confirmation happens in step 4.
3. `review-implementation`: fix actionable findings, then rebuild and retest.
4. Summarize the implementation and the review result, then ask the user whether the implementation is done as expected. Do not continue until the user confirms. If changes are requested, apply them and repeat steps 3 and 4.
5. `create-pull-request`: the user has confirmed in step 4, so do not ask again.

Never push to `main`.
13 changes: 13 additions & 0 deletions .claude/skills/create-branch/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: create-branch
description: Read a GitHub issue, then create and check out a branch named by repository convention. Use when starting work on an issue. Does not implement anything.
argument-hint: "[issue number]"
disable-model-invocation: true
---
Prepare a branch for the GitHub issue given as the argument (ask for the number if missing).

1. Read the issue (GitHub MCP tools) and check for existing branches/PRs for it; stop and report if one already exists.
2. Create the branch from the latest `main` using `<type>/<issue>_<kebab-description>` (types: `feature`, `bugfix`, `hotfix`, `chore`, `docs`).
3. Check out the branch locally.

Do not implement anything. Never push to `main`.
13 changes: 13 additions & 0 deletions .claude/skills/create-pull-request/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: create-pull-request
description: Commit, push and open a pull request for the current branch after the user has confirmed the implementation. Use when the work is done and reviewed.
argument-hint: "[issue number]"
disable-model-invocation: true
---
Open a pull request for the current branch (issue number given as the argument; infer from the branch name if missing).

1. Confirm with the user that the implementation is done as expected and has been reviewed; stop if not. Skip the question if the user already confirmed earlier in this session.
2. Run the build and tests; fix failures.
3. Commit as `#<issue> <Imperative summary>` and push the branch (never `main`).
4. Open a PR to `main` titled like the commit. Use `.github/pull_request_template.md` as the body and fill in every section.
5. Check the PR check runs and fix failures.
10 changes: 10 additions & 0 deletions .claude/skills/document-public-members/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
name: document-public-members
description: Add missing XML documentation (C#) or TSDoc (TypeScript) to public members in the selected files. Use when asked to document code. Never changes behavior.
---
Find public types and members in the selected file(s) that lack documentation and add it.

- C#: `<summary>`, plus `<param>`, `<returns>`, `<exception>`, `<typeparam>` where applicable; `<inheritdoc />` for overrides and interface implementations; document record parameters, enum values and constants.
- TypeScript: TSDoc on exported functions, hooks and components.
- Explain *why* and behavior, not what the name already says.
- Do not change code behavior.
14 changes: 14 additions & 0 deletions .claude/skills/implement/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
name: implement
description: Implement a GitHub issue on the current branch, including tests and docs, without committing. Use when the branch already exists. Use create-branch-implement-and-pr for the full flow.
argument-hint: "[issue number]"
disable-model-invocation: true
---
Implement the GitHub issue given as the argument (ask for the number if missing) on the current branch.

1. Read the issue (GitHub MCP tools).
2. Implement the change following `AGENTS.md` (code style, XML docs, tests, docs updates).
3. Build and run tests; fix failures.
4. Summarize the changes and ask the user whether the implementation is done as expected.

Do not commit, push or open a PR. Never work on `main`.
12 changes: 12 additions & 0 deletions .claude/skills/review-implementation/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
name: review-implementation
description: Review local uncommitted and unpushed changes against the Code Review checklist before a pull request exists. For an existing pull request use review-pull-request.
---
Review the uncommitted and unpushed changes on the current branch (diff against `main`).

1. Apply the Code Review checklist in `.github/copilot-instructions.md`.
2. Run the build and tests; report failures.
3. Report only actionable findings ordered by severity (security/correctness > contracts > tests > style), with a concrete fix for each.
4. Fix findings the user approves.

Do not commit, push or open a PR.
16 changes: 16 additions & 0 deletions .claude/skills/review-pull-request/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
name: review-pull-request
description: Review an existing GitHub pull request by number using the Code Review checklist and report findings without submitting a review. For local changes use review-implementation.
argument-hint: "[PR number]"
---
Review the pull request given as the argument (ask for the number if missing) using the GitHub MCP tools (details, diff, files, check runs, existing review comments).

Apply the "Code Review" checklist in `.github/copilot-instructions.md`.

Output:
1. Short summary of the change.
2. Findings grouped as Blocking / Suggestions / Nits, each with file, line and a concrete fix.
3. CI status and missing tests/docs.
4. Recommendation: approve, comment or request changes.

Do not submit the review unless asked.
12 changes: 12 additions & 0 deletions .claude/skills/security-check/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
name: security-check
description: Audit NuGet, .NET and npm dependencies for vulnerabilities and deprecated packages, read GitHub security alerts and fix findings at the root cause. Use for security reviews and before releases.
---
Follow "Dependency and Security Checking" in `AGENTS.md`.

1. Run `dotnet list src/Docentric.EInvoice.Validator.slnx package --vulnerable --include-transitive`, `--deprecated` and `--outdated`.
2. In `src/WebUI` run `npm audit` and `npm audit --omit=dev`.
3. Read open Dependabot, code scanning and secret scanning alerts (GitHub MCP tools).
4. Fix at the root cause: lowest patched version, direct reference for vulnerable transitive packages, `npm audit fix` before `--force`. Do not suppress warnings.
5. Rebuild, run tests and `npm run build`.
6. Summarize findings, fixes and anything left unresolved (with advisory IDs).
13 changes: 13 additions & 0 deletions .claude/skills/upgrade-dependency/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: upgrade-dependency
description: Upgrade one NuGet or npm package, or the Mustang CLI JAR, as a dedicated change with release note review. Use when asked to bump a dependency version.
argument-hint: "[package or Mustang CLI] [target version]"
disable-model-invocation: true
---
Upgrade the package given as the first argument to the version given as the second (ask if missing).

- Read the release notes/changelog for breaking changes and security fixes; never invent CLI options or APIs.
- Change only what the upgrade requires; for Mustang CLI replace the JAR and update every reference (project file, Dockerfile, README, code).
- For npm, commit `package-lock.json`; for NuGet, keep versions consistent across projects.
- Build with warnings as errors, run all tests, and verify the UI builds when relevant.
- Commit as `#<issue> Upgrade <package> to <version>` and note advisories or breaking changes in the PR.
Loading
Loading