Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 7 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ task procedures only when needed:
| Writing issues or PRDs, and triage labels | `docs/agents/issue-tracker.md`, `docs/agents/triage-labels.md` |
| Web backend setup or diagnostics | `docs/agents/web-backend.md` |

Versioned CLI help is the source of truth for command behavior. Start workflow planning with
`agent-device help workflow`, then use the relevant topic help.
Versioned CLI help is the source of truth for command behavior. Start with `agent-device help
workflow`, then the relevant topic help.

## Incident-derived principles

Expand Down Expand Up @@ -91,15 +91,16 @@ cross-language rules change through golden tables under `contracts/fixtures/`.
- Implementation files target at most 300 lines. Extract before adding behavior past 500 lines;
files past 1,000 lines are architecture debt unless generated or fixture data.
- Tests mirror source topology one-to-one. Split a source module and its test together; do not add to
the legacy `interaction.test.ts` or platform `index.test.ts` aggregations.
the legacy `interaction.test.ts` or platform `index.test.ts` aggregations. Pure moves carry their
tests unchanged; rename-only hunks owe no new coverage.
- Shared fixtures are named exports in a sibling fixture module, not repeated inline literals.
- `src/daemon/handlers/session.ts` is already over budget; extract the relevant platform-specific
concept before adding behavior.

## Toolchain and worktree traps

- Use `pnpm`; never add `package-lock.json`. OXC owns lint and format. Run `pnpm format` for the
repository, not a path-scoped formatter invocation.
- Use `pnpm`; never add `package-lock.json`. OXC owns lint and format. Run `pnpm format`
repository-wide, not path-scoped.
- A fresh worktree requires `pnpm install --frozen-lockfile && pnpm build`. Until then package and
optional-peer resolution may point at another checkout and produce false failures.
- Source-checkout daemon state is worktree-scoped, but devices are not. Use `pnpm daemon:state-dir`
Expand Down Expand Up @@ -155,5 +156,5 @@ contents. Keep a sentence in this file only when no gate, lint rule, versioned h
decision-site comment can own it. `CONTEXT.md` is glossary-only: no implementation paths,
architecture decisions, migration state, or workflows.

Behavior changes update their owning help/metadata and user docs when relevant. In the final summary,
Behavior changes update their owning help/metadata and user docs. In the final summary,
state whether docs or skills changed and why.
71 changes: 71 additions & 0 deletions scripts/coverage-changed/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,74 @@ test('changed line suppressed by an ignore directive is tallied, not gated', ()
test('threshold constant is the single source of truth', () => {
assert.equal(CHANGED_LINE_COVERAGE_THRESHOLD, 70);
});

// Shape of `git diff --unified=0 --find-renames=90%` for a move PR: a pure
// move carries no hunks, an edited move carries only the hunks that differ from
// its source, and an ordinary edit is unaffected.
const MOVE_PR_DIFF = [
'diff --git a/src/mod.ts b/src/mod.ts',
'index 57ee600..9fe64ef 100644',
'--- a/src/mod.ts',
'+++ b/src/mod.ts',
'@@ -2 +2,2 @@ export const m1 = 1;',
'-export const m2 = 2;',
'+export const m2 = 22;',
'+export const m3 = 3;',
'diff --git a/src/edited.ts b/src/moved-edited.ts',
'similarity index 90%',
'rename from src/edited.ts',
'rename to src/moved-edited.ts',
'index a3d7378..fe85380 100644',
'--- a/src/edited.ts',
'+++ b/src/moved-edited.ts',
'@@ -3 +3 @@ export const b2 = 2;',
'-export const b3 = 3;',
'+export const b3 = 33;',
'@@ -7 +7 @@ export const b6 = 6;',
'-export const b7 = 7;',
'+export const b7 = 77;',
'@@ -9 +9 @@ export const b8 = 8;',
'-export const b9 = 9;',
'+export const b9 = 99;',
'diff --git a/src/pure.ts b/src/moved-pure.ts',
'similarity index 100%',
'rename from src/pure.ts',
'rename to src/moved-pure.ts',
].join('\n');

function uncoveredRecord(file: string, lineCount: number): string {
const da = Array.from({ length: lineCount }, (_, i) => `DA:${i + 1},0`);
return [`SF:${file}`, ...da, 'end_of_record'].join('\n');
}

test('rename-only hunks owe no changed-line coverage; edited moves owe their edits', () => {
const diffs = parseUnifiedDiff(MOVE_PR_DIFF);
assert.deepEqual(
diffs.map((d) => [d.path, d.added]),
[
['src/mod.ts', [2, 3]],
['src/moved-edited.ts', [3, 7, 9]],
['src/moved-pure.ts', []],
],
);
const result = computeChangedCoverage({
diffs,
coverage: parseLcov(
[
uncoveredRecord('src/mod.ts', 3),
uncoveredRecord('src/moved-edited.ts', 30),
uncoveredRecord('src/moved-pure.ts', 30),
].join('\n'),
),
fileLines: () => null,
});
// 2 ordinary edits + 3 edited move lines; the 30-line pure move owes nothing.
assert.equal(result.totalLines, 5);
assert.deepEqual(
result.offenders.map((f) => [f.path, f.uncoveredLines]),
[
['src/mod.ts', [2, 3]],
['src/moved-edited.ts', [3, 7, 9]],
],
);
});
11 changes: 11 additions & 0 deletions scripts/coverage-changed/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,14 @@ test('errors when the lcov report is missing rather than silently passing', () =
assert.equal(code, 1);
assert.match(out, /no lcov report/);
});

test('a pure move owes nothing regardless of the host diff.renames setting', () => {
git('config', 'diff.renames', 'false');
git('mv', 'src/base.ts', 'src/moved.ts');
git('commit', '-q', '-m', 'move');
writeLcov('SF:src/moved.ts\nDA:1,0\nend_of_record\n');
const { code, out } = capture(() => run(['--base', 'main'], repo));
assert.equal(code, 0);
assert.match(out, /Changed-line coverage gate: PASS/);
assert.match(out, /0\/0 \(n\/a\)/);
});
14 changes: 9 additions & 5 deletions scripts/coverage-changed/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
//
// Reuses the lcov report that `pnpm test:coverage` already produced (never runs
// coverage a second time), joins it with `git diff --unified=0 <base>...HEAD`,
// and fails when changed-line coverage is below the threshold. A PR carrying
// and fails when changed-line coverage is below the threshold. Renames are
// detected at RENAME_SIMILARITY regardless of the host's `diff.renames`, so a
// moved file owes only the hunks that differ from its source. A PR carrying
// the `coverage-waiver` label skips the failure but still prints every number.
// The same markdown report goes to stdout and to the GitHub job summary; it
// includes changed-branch coverage and the count of changed executable lines
Expand All @@ -23,6 +25,7 @@ import {

const USAGE = 'Usage: pnpm check:coverage-changed [--base <ref>]\n';
const LCOV_PATH = 'coverage/lcov.info';
const RENAME_SIMILARITY = '90%';
const GIT_DIFF_MAX_BUFFER_BYTES = 16 * 1024 * 1024;

function fmtPct(pct: number | null): string {
Expand Down Expand Up @@ -72,10 +75,11 @@ export function run(argv: readonly string[], cwd?: string): number {
return 1;
}

const diff = runCmdSync('git', ['diff', '--unified=0', '--no-color', `${base}...HEAD`], {
cwd: root,
maxBuffer: GIT_DIFF_MAX_BUFFER_BYTES,
}).stdout;
const diff = runCmdSync(
'git',
['diff', '--unified=0', '--no-color', `--find-renames=${RENAME_SIMILARITY}`, `${base}...HEAD`],
{ cwd: root, maxBuffer: GIT_DIFF_MAX_BUFFER_BYTES },
).stdout;
const result = computeChangedCoverage({
diffs: parseUnifiedDiff(diff),
coverage: parseLcov(fs.readFileSync(lcovPath, 'utf8'), root),
Expand Down
Loading