Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e86fd4b
fix(cli): merge on import instead of overwriting the receiving home
kovrichard Aug 18, 2026
c44b9ae
test: make the suite order-independent under randomized execution
kovrichard Aug 18, 2026
2d0d294
chore: add stryker mutation testing with a diff-scoped pr gate
kovrichard Aug 18, 2026
60646b1
test(targets): cover the settings, cursor-hook, and vscode-path helpers
kovrichard Aug 18, 2026
f32b1bf
feat(project): add edit-isc to rewrite an ISC in place
kovrichard Aug 18, 2026
cfe1902
feat(project): add retire-isc for criteria that stopped being valid
kovrichard Aug 18, 2026
a2eef51
chore(biome): exclude the install smoke-test home from the check scope
kovrichard Aug 18, 2026
4b0d75b
perf(hooks): skip the session-end gates on a clean worktree
kovrichard Aug 18, 2026
e93ab59
chore: add secretlint to the gate chain
kovrichard Aug 18, 2026
822508f
chore: add madge to the gate chain
kovrichard Aug 18, 2026
69576c3
chore: add a line-ending gate with a fix mode
kovrichard Aug 18, 2026
dd8d547
chore(stryker): make the mutation gate blocking at a 50 percent thres…
kovrichard Aug 18, 2026
9591e40
test(targets): cover per-platform agent extraction and removal
kovrichard Aug 18, 2026
02377fc
refactor(targets): resolve installer paths per call instead of at import
kovrichard Aug 18, 2026
cec9f15
test(targets): cover the skill index and personal skill linking
kovrichard Aug 18, 2026
91cfeb7
test(targets): cover telos, settings, docs, statusline, and skill ins…
kovrichard Aug 18, 2026
de135e8
chore(stryker): raise the mutation threshold to 54 percent
kovrichard Aug 18, 2026
7114c2b
test(hooks): cover relationship note storage and recall
kovrichard Aug 18, 2026
55f80ca
test(tools): cover synthesis of ratings, reflections, and sessions
kovrichard Aug 18, 2026
8d59446
chore(stryker): raise the mutation threshold to 57 percent
kovrichard Aug 18, 2026
d08fee0
test(hooks): cover stop-handler caching and pending-failure claiming
kovrichard Aug 18, 2026
a1f7e89
test(hooks): cover context assembly with content assertions
kovrichard Aug 18, 2026
513f2b3
chore(stryker): scope the ratchet to the diff gate
kovrichard Aug 18, 2026
4544535
fix(test): locate the quarantined copy with a portable directory walk
kovrichard Aug 18, 2026
c08f347
chore(klint): upgrade to 0.34.0 and install the skill as copies
kovrichard Aug 18, 2026
3139113
fix(hooks): claim a pending failure inside the state directory
kovrichard Aug 18, 2026
14432c7
ci: pin actions to commit shas and install without lifecycle scripts
kovrichard Aug 18, 2026
5e464da
chore(copilot): gate the session end for Copilot too
kovrichard Aug 18, 2026
07896c8
feat(machine): give each install a stable identity that records can r…
kovrichard Aug 18, 2026
15ef621
test(machine): close the mutation gaps the ratchet exposed
kovrichard Aug 18, 2026
c26ea32
Merge branch 'main' into feat/portable-memory
kovrichard Aug 18, 2026
f270aaa
feat(anchor): resolve project-relative paths instead of raw absolute …
kovrichard Aug 18, 2026
7b64e6a
feat(signals): stamp every signal with its emitting machine's id
kovrichard Aug 18, 2026
c3762d8
feat(relationship): anchor the session comment's cwd stamp
kovrichard Aug 18, 2026
45dc414
feat(algorithm-reflect): stamp cwd anchor and machine id on every ref…
kovrichard Aug 18, 2026
3aef6de
feat(thread): stamp cwd anchor and machine id on every thread
kovrichard Aug 18, 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
4 changes: 4 additions & 0 deletions .agents/hooks/lf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { runHook } from "./run-hook";

const exitCode = runHook(["bun", "run", "lf"]);
process.exit(exitCode);
4 changes: 4 additions & 0 deletions .agents/hooks/madge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { runHook } from "./run-hook";

const exitCode = runHook(["bun", "run", "madge"]);
process.exit(exitCode);
31 changes: 26 additions & 5 deletions .agents/hooks/run-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,25 @@ function writeFailure(format: HookFormat, output: string): void {
process.stderr.write(output);
}

function writeSuccess(format: HookFormat, output: string): number {
if (format === "codex") return 0;
process.stdout.write(JSON.stringify({ output }));
return 0;
}

// Fails closed: a non-zero git status (not a repo, git missing, index locked)
// reports changes so the gates still run. Only a confirmed-empty status skips.
// --porcelain=v1 lists untracked files too, so a new file counts as a change.
function worktreeHasChanges(): boolean {
const r = spawnSync("git status --porcelain=v1", {
encoding: "utf8",
shell: true,
stdio: ["ignore", "pipe", "pipe"],
});
if ((r.status ?? -1) !== 0) return true;
return (r.stdout ?? "").trim().length > 0;
}

// Helper for agent hook scripts. Each hook file (lint.ts, test.ts, ...) is a
// thin wrapper that calls runHook(["bun", "run", "<script>"]). We capture
// stdout+stderr, return them on success in a JSON envelope (so Claude/opencode
Expand All @@ -28,6 +47,12 @@ export function runHook(args: string[], format = hookFormatFromArgs()): number {
writeFailure(format, "run-hook: no command provided");
return 2;
}
if (!worktreeHasChanges()) {
return writeSuccess(
format,
"skipped: worktree clean, HEAD already gated by pre-commit and CI"
);
}
const command = args.join(" ");
const r = spawnSync(command, {
encoding: "utf8",
Expand All @@ -38,11 +63,7 @@ export function runHook(args: string[], format = hookFormatFromArgs()): number {
const output = out || "(no output)";
const ok = (r.status ?? -1) === 0;

if (ok) {
if (format === "codex") return 0;
process.stdout.write(JSON.stringify({ output: "ok" }));
return 0;
}
if (ok) return writeSuccess(format, "ok");
writeFailure(format, output);
return 2;
}
Expand Down
4 changes: 4 additions & 0 deletions .agents/hooks/secretlint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { runHook } from "./run-hook";

const exitCode = runHook(["bun", "run", "secretlint"]);
process.exit(exitCode);
62 changes: 62 additions & 0 deletions .agents/scripts/check-lf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { spawnSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";

function listTrackedFilesWithEol(): string[] {
const r = spawnSync("git", ["ls-files", "--eol"], { encoding: "utf8" });
if (r.status !== 0) {
process.stderr.write(r.stderr || "git ls-files --eol failed\n");
process.exit(1);
}
return r.stdout.split("\n").filter(Boolean);
}

export function hasCarriageReturn(eolLine: string): boolean {
const [index, worktree] = eolLine.split(/\s+/);
return /crlf|mixed/.test(index) || /crlf|mixed/.test(worktree);
}

function pathFrom(eolLine: string): string {
return eolLine.split("\t").slice(1).join("\t");
}

// A NUL byte is git's own heuristic for binary content. Rewriting CR bytes inside
// an image or archive would corrupt it, so those are reported and left alone.
export function isBinary(contents: Buffer): boolean {
return contents.includes(0);
}

function convertToLf(path: string): boolean {
const contents = readFileSync(path);
if (isBinary(contents)) return false;
writeFileSync(path, contents.toString("utf8").replaceAll("\r\n", "\n"), "utf8");
return true;
}

function reportAndExit(offenders: string[]): never {
process.stderr.write(
`CRLF found in tracked files (LF required):\n${offenders.join("\n")}\n` +
`Run 'bun run lf:fix' to convert them.\n`
);
process.exit(1);
}

function fixAndExit(offenders: string[]): never {
const skipped = offenders.filter((path) => !convertToLf(path));
const converted = offenders.length - skipped.length;
process.stdout.write(`Converted ${converted} file(s) to LF.\n`);
if (skipped.length === 0) process.exit(0);
process.stderr.write(`Skipped binary file(s):\n${skipped.join("\n")}\n`);
process.exit(1);
}

if (import.meta.main) {
const offenders = listTrackedFilesWithEol().filter(hasCarriageReturn).map(pathFrom);

if (offenders.length === 0) {
process.stdout.write("All tracked files use LF line endings.\n");
process.exit(0);
}

if (process.argv.includes("--fix")) fixAndExit(offenders);
reportAndExit(offenders);
}
97 changes: 97 additions & 0 deletions .agents/scripts/mutate-diff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { spawnSync } from "node:child_process";
import { Glob } from "bun";
import strykerConfig from "../../stryker.config.mjs";

const baseRef = process.argv[2] ?? "origin/main";

function mutatePatterns(): string[] {
const patterns = strykerConfig.mutate ?? [];
return patterns.filter((pattern): pattern is string => typeof pattern === "string");
}

type MutateFilter = { includes: Glob[]; excludes: Glob[] };

function mutateFilter(patterns: string[]): MutateFilter {
const includes: Glob[] = [];
const excludes: Glob[] = [];
for (const pattern of patterns) {
if (pattern.startsWith("!")) {
excludes.push(new Glob(pattern.slice(1)));
} else {
includes.push(new Glob(pattern));
}
}
return { includes, excludes };
}

function isMutatable(path: string, filter: MutateFilter): boolean {
if (filter.excludes.some((glob) => glob.match(path))) {
return false;
}
return filter.includes.some((glob) => glob.match(path));
}

function diffAgainstBase(): string {
const r = spawnSync(
"git",
["diff", "--unified=0", "--diff-filter=ACMR", `${baseRef}...HEAD`],
{ encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }
);
if (r.status !== 0) {
process.stderr.write(r.stderr || `git diff against ${baseRef} failed\n`);
process.exit(1);
}
return r.stdout;
}

function changedRanges(diff: string, filter: MutateFilter): string[] {
const ranges: string[] = [];
let current: string | null = null;

for (const line of diff.split("\n")) {
const fileMatch = /^\+\+\+ b\/(.+)$/.exec(line);
if (fileMatch) {
const path = fileMatch[1];
current = isMutatable(path, filter) ? path : null;
continue;
}

if (!current) continue;

const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(line);
if (!hunk) continue;

const start = Number(hunk[1]);
const count = hunk[2] === undefined ? 1 : Number(hunk[2]);
if (count === 0) continue;

ranges.push(`${current}:${start}-${start + count - 1}`);
}

return ranges;
}

function runStryker(ranges: string[]): number {
const r = spawnSync("bunx", ["stryker", "run", "--mutate", ranges.join(",")], {
stdio: "inherit",
});
return r.status ?? 1;
}

const patterns = mutatePatterns();
const ranges = changedRanges(diffAgainstBase(), mutateFilter(patterns));

process.stderr.write(
`Mutation gate covers ${patterns.length} glob(s): ${patterns.join(", ")}\n` +
`Changes outside these paths are NOT mutation-tested.\n`
);

if (ranges.length === 0) {
process.stderr.write(`No mutatable changes against ${baseRef} — nothing to gate.\n`);
process.exit(0);
}

process.stderr.write(
`Mutating ${ranges.length} changed range(s):\n${ranges.join("\n")}\n`
);
process.exit(runStryker(ranges));
1 change: 0 additions & 1 deletion .agents/skills/klint-rules

This file was deleted.

4 changes: 4 additions & 0 deletions .agents/skills/klint-rules/.klint-skill.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"version": "0.34.0",
"sha256": "c8c9584cee2ef21246fc0d0498e39d7a1af7b965aff875bbb8f55c608004357b"
}
Loading
Loading