@ikon85/agent-workflow-kit 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,6 +24,7 @@ This is a prompt-driven skill, not a deterministic script. Explore, present what
24
24
  | `docs/agents/code-review.md` | Standards-source pointers + adjacent-review-tooling notes the `code-review` skill's Standards axis reads (Section I) |
25
25
  | `## Workflow` in CLAUDE.md **and** AGENTS.md | generic entry-point map seeded from [workflow-overview.md](./workflow-overview.md) (Section F + Write) |
26
26
  | `## Agent skills` + `## Prod` in CLAUDE.md **and** AGENTS.md | one-line pointers + deploy target (Sections C/G + Write) |
27
+ | `.github/workflows/agent-workflow-kit-update.yml` | optional tested Kit update pull request for GitHub consumers (Section A2) |
27
28
 
28
29
  ## Idempotency contract — read before writing anything
29
30
 
@@ -68,6 +69,40 @@ Default posture: these skills were designed for GitHub. If a `git remote` points
68
69
 
69
70
  Seed `docs/agents/issue-tracker.md` from the matching template in this folder: [issue-tracker-github.md](./issue-tracker-github.md), [issue-tracker-gitlab.md](./issue-tracker-gitlab.md), [issue-tracker-local.md](./issue-tracker-local.md). For "other", write it from the user's description.
70
71
 
72
+ ### 2a. Automatic Kit update pull requests (GitHub tracker only)
73
+
74
+ > A short scheduled check can keep the installed Kit current by opening one normal pull request after the candidate passes the consumer's own tests. It never merges the pull request for you.
75
+
76
+ Only after the user has confirmed a **GitHub tracker**, ask in plain language: *"Should GitHub check weekly for a tested Agent Workflow Kit update and keep one update pull request ready?"* Offer these explicit choices:
77
+
78
+ Before offering **Enable**, confirm the consumer has a committed `package-lock.json` and a usable `package.json` `npm test` command. The shipped workflow installs the locked dependencies with `npm ci --ignore-scripts`; without that lockfile/test contract it cannot prove the candidate in a clean checkout, so explain the prerequisite and do not create the workflow yet.
79
+
80
+ Also read the repository Actions policy before enabling:
81
+
82
+ ```bash
83
+ gh api repos/<owner>/<repo>/actions/permissions/workflow
84
+ ```
85
+
86
+ The response must report `"can_approve_pull_request_reviews": true`; this is GitHub's **Allow GitHub Actions to create and approve pull requests** gate used by the stable PR upsert. If it is false, or the policy cannot be read, do not seed the workflow yet. Guide the user to **Settings → Actions → General → Workflow permissions**, enable that checkbox, and rerun `setup-workflow`.
87
+
88
+ If the user prefers `gh`, first show the exact mutation and obtain **explicit confirmation**; never change repository settings merely because setup was invoked. After confirmation, preserve the currently reported `default_workflow_permissions` value (`read` or `write`) and set the gate with:
89
+
90
+ ```bash
91
+ gh api --method PUT repos/<owner>/<repo>/actions/permissions/workflow \
92
+ -f default_workflow_permissions=<current-read-or-write> \
93
+ -F can_approve_pull_request_reviews=true
94
+ ```
95
+
96
+ Read the policy back and offer **Enable** only after the field is actually true.
97
+
98
+ - **Enable** — seed `.github/workflows/agent-workflow-kit-update.yml` from [assets/agent-workflow-kit-update.yml](./assets/agent-workflow-kit-update.yml).
99
+ - **Opt out** — do not create a GitHub workflow, branch, or pull request.
100
+ - **Ask later** — do not create a GitHub workflow, branch, or pull request; explain that re-running `setup-workflow` can offer it again.
101
+
102
+ This workflow opt-in has its own file-level idempotency rule: if the destination is already present, leave it byte-for-byte unchanged and report `skipped (already present)`. On repeated setup runs, do not prompt again when it exists. The workflow consumes the scoped `@ikon85/agent-workflow-kit` release and its existing parity-checked transactional update command; do not reproduce either mechanism in setup prose or shell steps.
103
+
104
+ For GitLab, local, other, or an unknown provider, give only a provider-neutral explanation that automatic updates require provider-specific CI and pull-request support; **do not create a GitHub workflow**. Setup itself never creates the update branch or pull request — only an opted-in workflow run may do that later.
105
+
71
106
  **Conditional board-write note (only when the GitHub tracker uses a managed board):** if Section D ends with `board-sync.md` at `mode: github-projects-v2`, add one line to `issue-tracker.md`: *"Board writes (item-add, status/wave/cluster field edits, sub-issue links) go through the board-sync helper, not bare `gh issue create`/`gh project item-*`."* Do **not** add this for GitLab, local, other, or `mode: none`.
72
107
 
73
108
  ### 3. Section B — Triage labels
@@ -0,0 +1,31 @@
1
+ name: Agent Workflow Kit update
2
+
3
+ on:
4
+ schedule:
5
+ - cron: '17 6 * * 1'
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: write
10
+ pull-requests: write
11
+
12
+ concurrency:
13
+ group: agent-workflow-kit-update
14
+ cancel-in-progress: false
15
+
16
+ jobs:
17
+ update:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ with:
22
+ fetch-depth: 0
23
+ - uses: actions/setup-node@v4
24
+ with:
25
+ node-version: 22.14
26
+ - name: Install locked consumer dependencies
27
+ run: npm ci --ignore-scripts
28
+ - name: Verify, test, and upsert the Kit update pull request
29
+ env:
30
+ GH_TOKEN: ${{ github.token }}
31
+ run: npx --yes --package=@ikon85/agent-workflow-kit@latest agent-workflow-kit-update-pr
@@ -24,6 +24,7 @@ This is a prompt-driven skill, not a deterministic script. Explore, present what
24
24
  | `docs/agents/code-review.md` | Standards-source pointers + adjacent-review-tooling notes the `code-review` skill's Standards axis reads (Section I) |
25
25
  | `## Workflow` in CLAUDE.md **and** AGENTS.md | generic entry-point map seeded from [workflow-overview.md](./workflow-overview.md) (Section F + Write) |
26
26
  | `## Agent skills` + `## Prod` in CLAUDE.md **and** AGENTS.md | one-line pointers + deploy target (Sections C/G + Write) |
27
+ | `.github/workflows/agent-workflow-kit-update.yml` | optional tested Kit update pull request for GitHub consumers (Section A2) |
27
28
 
28
29
  ## Idempotency contract — read before writing anything
29
30
 
@@ -68,6 +69,40 @@ Default posture: these skills were designed for GitHub. If a `git remote` points
68
69
 
69
70
  Seed `docs/agents/issue-tracker.md` from the matching template in this folder: [issue-tracker-github.md](./issue-tracker-github.md), [issue-tracker-gitlab.md](./issue-tracker-gitlab.md), [issue-tracker-local.md](./issue-tracker-local.md). For "other", write it from the user's description.
70
71
 
72
+ ### 2a. Automatic Kit update pull requests (GitHub tracker only)
73
+
74
+ > A short scheduled check can keep the installed Kit current by opening one normal pull request after the candidate passes the consumer's own tests. It never merges the pull request for you.
75
+
76
+ Only after the user has confirmed a **GitHub tracker**, ask in plain language: *"Should GitHub check weekly for a tested Agent Workflow Kit update and keep one update pull request ready?"* Offer these explicit choices:
77
+
78
+ Before offering **Enable**, confirm the consumer has a committed `package-lock.json` and a usable `package.json` `npm test` command. The shipped workflow installs the locked dependencies with `npm ci --ignore-scripts`; without that lockfile/test contract it cannot prove the candidate in a clean checkout, so explain the prerequisite and do not create the workflow yet.
79
+
80
+ Also read the repository Actions policy before enabling:
81
+
82
+ ```bash
83
+ gh api repos/<owner>/<repo>/actions/permissions/workflow
84
+ ```
85
+
86
+ The response must report `"can_approve_pull_request_reviews": true`; this is GitHub's **Allow GitHub Actions to create and approve pull requests** gate used by the stable PR upsert. If it is false, or the policy cannot be read, do not seed the workflow yet. Guide the user to **Settings → Actions → General → Workflow permissions**, enable that checkbox, and rerun `setup-workflow`.
87
+
88
+ If the user prefers `gh`, first show the exact mutation and obtain **explicit confirmation**; never change repository settings merely because setup was invoked. After confirmation, preserve the currently reported `default_workflow_permissions` value (`read` or `write`) and set the gate with:
89
+
90
+ ```bash
91
+ gh api --method PUT repos/<owner>/<repo>/actions/permissions/workflow \
92
+ -f default_workflow_permissions=<current-read-or-write> \
93
+ -F can_approve_pull_request_reviews=true
94
+ ```
95
+
96
+ Read the policy back and offer **Enable** only after the field is actually true.
97
+
98
+ - **Enable** — seed `.github/workflows/agent-workflow-kit-update.yml` from [assets/agent-workflow-kit-update.yml](./assets/agent-workflow-kit-update.yml).
99
+ - **Opt out** — do not create a GitHub workflow, branch, or pull request.
100
+ - **Ask later** — do not create a GitHub workflow, branch, or pull request; explain that re-running `setup-workflow` can offer it again.
101
+
102
+ This workflow opt-in has its own file-level idempotency rule: if the destination is already present, leave it byte-for-byte unchanged and report `skipped (already present)`. On repeated setup runs, do not prompt again when it exists. The workflow consumes the scoped `@ikon85/agent-workflow-kit` release and its existing parity-checked transactional update command; do not reproduce either mechanism in setup prose or shell steps.
103
+
104
+ For GitLab, local, other, or an unknown provider, give only a provider-neutral explanation that automatic updates require provider-specific CI and pull-request support; **do not create a GitHub workflow**. Setup itself never creates the update branch or pull request — only an opted-in workflow run may do that later.
105
+
71
106
  **Conditional board-write note (only when the GitHub tracker uses a managed board):** if Section D ends with `board-sync.md` at `mode: github-projects-v2`, add one line to `issue-tracker.md`: *"Board writes (item-add, status/wave/cluster field edits, sub-issue links) go through the board-sync helper, not bare `gh issue create`/`gh project item-*`."* Do **not** add this for GitLab, local, other, or `mode: none`.
72
107
 
73
108
  ### 3. Section B — Triage labels
@@ -0,0 +1,31 @@
1
+ name: Agent Workflow Kit update
2
+
3
+ on:
4
+ schedule:
5
+ - cron: '17 6 * * 1'
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: write
10
+ pull-requests: write
11
+
12
+ concurrency:
13
+ group: agent-workflow-kit-update
14
+ cancel-in-progress: false
15
+
16
+ jobs:
17
+ update:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ with:
22
+ fetch-depth: 0
23
+ - uses: actions/setup-node@v4
24
+ with:
25
+ node-version: 22.14
26
+ - name: Install locked consumer dependencies
27
+ run: npm ci --ignore-scripts
28
+ - name: Verify, test, and upsert the Kit update pull request
29
+ env:
30
+ GH_TOKEN: ${{ github.token }}
31
+ run: npx --yes --package=@ikon85/agent-workflow-kit@latest agent-workflow-kit-update-pr
package/README.md CHANGED
@@ -332,6 +332,23 @@ still reference. Flags: `--force` (overwrite pre-existing files on `init`),
332
332
 
333
333
  ## Release notes
334
334
 
335
+ ### 0.14.0
336
+
337
+ - added: `scripts/census/delta.mjs`
338
+ - added: `scripts/census/fingerprint.mjs`
339
+ - added: `scripts/census/index.mjs`
340
+ - added: `scripts/census/scan.mjs`
341
+ - added: `scripts/census/state.mjs`
342
+ - added: `scripts/census/transaction.mjs`
343
+
344
+ ### 0.13.0
345
+
346
+ - added: `.agents/skills/setup-workflow/assets/agent-workflow-kit-update.yml`
347
+ - added: `.claude/skills/setup-workflow/assets/agent-workflow-kit-update.yml`
348
+ - added: `scripts/kit-update-pr.mjs`
349
+ - changed: `.agents/skills/setup-workflow/SKILL.md`
350
+ - changed: `.claude/skills/setup-workflow/SKILL.md`
351
+
335
352
  ### 0.12.0
336
353
 
337
354
  - added: `.agents/skills/kit-update/SKILL.md`
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitVersion": "0.12.0",
2
+ "kitVersion": "0.14.0",
3
3
  "files": [
4
4
  {
5
5
  "path": ".agents/skills/ask-matt/SKILL.md",
@@ -460,6 +460,15 @@
460
460
  "mode": 420,
461
461
  "origin": "kit"
462
462
  },
463
+ {
464
+ "path": ".agents/skills/setup-workflow/assets/agent-workflow-kit-update.yml",
465
+ "kind": "skill",
466
+ "ownerSkill": "setup-workflow",
467
+ "surface": "codex",
468
+ "sha256": "d8abf995d67184b4459b9565c9062319fc42e8a685042b288b3066a07a6fda59",
469
+ "mode": 420,
470
+ "origin": "kit"
471
+ },
463
472
  {
464
473
  "path": ".agents/skills/setup-workflow/board-sync.md",
465
474
  "kind": "skill",
@@ -528,7 +537,7 @@
528
537
  "kind": "skill",
529
538
  "ownerSkill": "setup-workflow",
530
539
  "surface": "codex",
531
- "sha256": "ea0b75ccd1579820a09e7c8d4c151126aa0be7a0be6b76b23599fadebb44b554",
540
+ "sha256": "efc7818d98158a85a79f3a652787d2e557552fa5175b32b24916c894388b9b74",
532
541
  "mode": 420,
533
542
  "origin": "kit"
534
543
  },
@@ -1433,6 +1442,15 @@
1433
1442
  "mode": 420,
1434
1443
  "origin": "kit"
1435
1444
  },
1445
+ {
1446
+ "path": ".claude/skills/setup-workflow/assets/agent-workflow-kit-update.yml",
1447
+ "kind": "skill",
1448
+ "ownerSkill": "setup-workflow",
1449
+ "surface": "claude",
1450
+ "sha256": "d8abf995d67184b4459b9565c9062319fc42e8a685042b288b3066a07a6fda59",
1451
+ "mode": 420,
1452
+ "origin": "kit"
1453
+ },
1436
1454
  {
1437
1455
  "path": ".claude/skills/setup-workflow/board-sync.md",
1438
1456
  "kind": "skill",
@@ -1501,7 +1519,7 @@
1501
1519
  "kind": "skill",
1502
1520
  "ownerSkill": "setup-workflow",
1503
1521
  "surface": "claude",
1504
- "sha256": "ea0b75ccd1579820a09e7c8d4c151126aa0be7a0be6b76b23599fadebb44b554",
1522
+ "sha256": "efc7818d98158a85a79f3a652787d2e557552fa5175b32b24916c894388b9b74",
1505
1523
  "mode": 420,
1506
1524
  "origin": "kit"
1507
1525
  },
@@ -1862,6 +1880,48 @@
1862
1880
  "mode": 493,
1863
1881
  "origin": "kit"
1864
1882
  },
1883
+ {
1884
+ "path": "scripts/census/delta.mjs",
1885
+ "kind": "script",
1886
+ "sha256": "b4f92ecb4bf1920bd62707c83f308269385bc5e35c2205abc97d8d8b849ade84",
1887
+ "mode": 420,
1888
+ "origin": "kit"
1889
+ },
1890
+ {
1891
+ "path": "scripts/census/fingerprint.mjs",
1892
+ "kind": "script",
1893
+ "sha256": "fbb25d9f53ab74a9bdffc5db816a56f0d976204b74c2126b8ec7c11e8c8d54fc",
1894
+ "mode": 420,
1895
+ "origin": "kit"
1896
+ },
1897
+ {
1898
+ "path": "scripts/census/index.mjs",
1899
+ "kind": "script",
1900
+ "sha256": "ef87d073842dec13f27c929e573e064314fdd704aeeec85e80d526c044f23a9a",
1901
+ "mode": 420,
1902
+ "origin": "kit"
1903
+ },
1904
+ {
1905
+ "path": "scripts/census/scan.mjs",
1906
+ "kind": "script",
1907
+ "sha256": "0988282c8aa6f1e1644eab924fab6e5e306c5a8bb3c26175ff74080d8de673d9",
1908
+ "mode": 420,
1909
+ "origin": "kit"
1910
+ },
1911
+ {
1912
+ "path": "scripts/census/state.mjs",
1913
+ "kind": "script",
1914
+ "sha256": "588fb5bd3a3855fd7acdf9c056c63e0a04f94308b91f48b9be18807ca849fab7",
1915
+ "mode": 420,
1916
+ "origin": "kit"
1917
+ },
1918
+ {
1919
+ "path": "scripts/census/transaction.mjs",
1920
+ "kind": "script",
1921
+ "sha256": "63a27f4b28583178bfc08fd6ac553528d9d955e1ec06bc37f779824d5fcaa92f",
1922
+ "mode": 420,
1923
+ "origin": "kit"
1924
+ },
1865
1925
  {
1866
1926
  "path": "scripts/execute-ready-check.py",
1867
1927
  "kind": "script",
@@ -1883,6 +1943,13 @@
1883
1943
  "mode": 420,
1884
1944
  "origin": "kit"
1885
1945
  },
1946
+ {
1947
+ "path": "scripts/kit-update-pr.mjs",
1948
+ "kind": "script",
1949
+ "sha256": "64e7818003d7c6db227eadad0b114e199399a52a96965ab2f0f496c0a1e5c79c",
1950
+ "mode": 493,
1951
+ "origin": "kit"
1952
+ },
1886
1953
  {
1887
1954
  "path": "scripts/loc_offender_core.py",
1888
1955
  "kind": "script",
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@ikon85/agent-workflow-kit",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Portable AI-agent workflow skills (plan → execute → land → learn) for Claude Code & Codex — grilling, TDD, diagnosis, two-axis code review, cross-model Codex review, design & domain-modeling, plus a skill router (ask-matt). npx init/update/diff/uninstall.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "agent-workflow-kit": "src/cli.mjs"
7
+ "agent-workflow-kit": "src/cli.mjs",
8
+ "agent-workflow-kit-update-pr": "scripts/kit-update-pr.mjs"
8
9
  },
9
10
  "scripts": {
10
11
  "test": "npm run test:node && npm run test:python",
@@ -37,6 +37,9 @@ test('current build contains post-tag public files and repository metadata', asy
37
37
  const pkg = JSON.parse(await readFile(join(dist, 'package.json'), 'utf8'));
38
38
  assert.equal(pkg.name, '@ikon85/agent-workflow-kit');
39
39
  assert.equal(pkg.repository.url, 'git+https://github.com/iKon85/agent-workflow-kit.git');
40
+ assert.equal(pkg.bin['agent-workflow-kit'], 'src/cli.mjs');
41
+ assert.equal(pkg.bin['agent-workflow-kit-update-pr'], 'scripts/kit-update-pr.mjs');
42
+ await readFile(join(dist, 'scripts/kit-update-pr.mjs'));
40
43
  });
41
44
  });
42
45
 
@@ -52,9 +55,31 @@ test('npm pack keeps the complete scripts tree but excludes Python caches', () =
52
55
  const files = JSON.parse(output)[0].files.map((file) => file.path);
53
56
  assert.ok(files.includes('scripts/build-kit.mjs'));
54
57
  assert.ok(files.includes('scripts/board-sync.py'));
58
+ assert.ok(files.includes('scripts/kit-update-pr.mjs'));
55
59
  assert.ok(files.every((path) => !path.includes('__pycache__') && !path.endsWith('.pyc')));
56
60
  const pkg = JSON.parse(execFileSync('node', ['-p', 'JSON.stringify(require("./package.json"))'], {
57
61
  cwd: REPO, encoding: 'utf8',
58
62
  }));
59
63
  assert.ok(pkg.files.includes('scripts/'), 'package must ship future script file types');
60
64
  });
65
+
66
+ test('packed scoped artifact keeps the existing npx default-bin inference', async () => {
67
+ const destination = await mkdtemp(join(tmpdir(), 'awkit-pack-'));
68
+ try {
69
+ const packed = JSON.parse(execFileSync('npm', [
70
+ 'pack', '--pack-destination', destination, '--json',
71
+ ], { cwd: REPO, encoding: 'utf8' }));
72
+ const output = execFileSync('npx', [
73
+ '--yes', '--offline', `./${packed[0].filename}`,
74
+ ], { cwd: destination, encoding: 'utf8' });
75
+ assert.match(output, /Usage: agent-workflow-kit/);
76
+
77
+ const updater = execFileSync('npm', [
78
+ 'exec', '--yes', '--offline', `--package=./${packed[0].filename}`,
79
+ '--', 'agent-workflow-kit-update-pr', 'help',
80
+ ], { cwd: destination, encoding: 'utf8' });
81
+ assert.match(updater, /Usage: agent-workflow-kit-update-pr/);
82
+ } finally {
83
+ await rm(destination, { recursive: true, force: true });
84
+ }
85
+ });
@@ -0,0 +1,252 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { cp, mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises';
4
+ import { execFile } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import { tmpdir } from 'node:os';
7
+ import { join } from 'node:path';
8
+ import {
9
+ CENSUS_VERDICTS,
10
+ diffCensus,
11
+ scanCensus,
12
+ serializeCensus,
13
+ } from './index.mjs';
14
+
15
+ const exec = promisify(execFile);
16
+ const FIXTURE = new URL('../../test/fixtures/census-recipes/known/', import.meta.url);
17
+
18
+ async function makeRepository() {
19
+ const root = await mkdtemp(join(tmpdir(), 'awk-census-'));
20
+ await cp(FIXTURE, root, { recursive: true });
21
+ await exec('git', ['init', '--quiet'], { cwd: root });
22
+ await exec('git', ['add', '.'], { cwd: root });
23
+ await exec('git', ['add', '-f', 'ignored/src/ignored.mjs'], { cwd: root });
24
+ return root;
25
+ }
26
+
27
+ test('known recipe scans are byte-identical and separate denominator from evidence', async () => {
28
+ const root = await makeRepository();
29
+ try {
30
+ const first = await scanCensus({ repoRoot: root });
31
+ const second = await scanCensus({ repoRoot: root });
32
+
33
+ assert.equal(serializeCensus(first), serializeCensus(second));
34
+ assert.deepEqual(first.denominator.map(({ path }) => path), [
35
+ 'package.json',
36
+ 'src/index.mjs',
37
+ ]);
38
+ assert.deepEqual(first.evidence.map(({ path }) => path), [
39
+ 'docs/architecture.md',
40
+ 'test/index.test.mjs',
41
+ ]);
42
+ assert.equal(first.fingerprints.builder.length, 64);
43
+ assert.equal(first.fingerprints.topology.length, 64);
44
+ } finally {
45
+ await rm(root, { recursive: true, force: true });
46
+ }
47
+ });
48
+
49
+ test('scan state reflects explicit activation and active-census facts', async () => {
50
+ const root = await makeRepository();
51
+ try {
52
+ assert.equal((await scanCensus({ repoRoot: root })).state, 'disabled');
53
+ assert.equal((await scanCensus({ repoRoot: root, enabled: true })).state, 'bootstrap');
54
+ assert.equal((await scanCensus({ repoRoot: root, enabled: true, hasActive: true })).state, 'current');
55
+
56
+ await mkdir(join(root, 'new-service', 'src'), { recursive: true });
57
+ await writeFile(join(root, 'new-service', 'src', 'index.mjs'), 'export const newService = true;\n');
58
+ assert.equal(
59
+ (await scanCensus({ repoRoot: root, enabled: true, hasActive: true })).state,
60
+ 'refresh_required',
61
+ );
62
+ } finally {
63
+ await rm(root, { recursive: true, force: true });
64
+ }
65
+ });
66
+
67
+ test('secret, ignored, generated, and vendor paths are never read or counted', async () => {
68
+ const root = await makeRepository();
69
+ try {
70
+ const reads = [];
71
+ const result = await scanCensus({
72
+ repoRoot: root,
73
+ readText: async (path) => {
74
+ reads.push(path);
75
+ return (await import('node:fs/promises')).readFile(path, 'utf8');
76
+ },
77
+ });
78
+ const output = serializeCensus(result);
79
+
80
+ assert.ok(!reads.some((path) => path.includes('secrets')));
81
+ assert.ok(!reads.some((path) => path.includes('credentials')));
82
+ assert.ok(!output.includes('CENSUS_SECRET_CANARY'));
83
+ assert.ok(!output.includes('secrets/'));
84
+ assert.ok(!output.includes('credentials.txt'));
85
+ assert.ok(!output.includes('vendor/src'));
86
+ assert.ok(!output.includes('dist/src'));
87
+ assert.ok(!output.includes('ignored/src'));
88
+ assert.equal(result.denominator.length, 2);
89
+ } finally {
90
+ await rm(root, { recursive: true, force: true });
91
+ }
92
+ });
93
+
94
+ test('tracked product symlinks stay unread and force an open refresh', async () => {
95
+ const root = await makeRepository();
96
+ const external = await mkdtemp(join(tmpdir(), 'awk-census-external-'));
97
+ const externalPath = join(external, 'canary.mjs');
98
+ await writeFile(externalPath, 'CENSUS_EXTERNAL_SECRET_CANARY\n');
99
+ await symlink(externalPath, join(root, 'src', 'external.mjs'));
100
+ await exec('git', ['add', 'src/external.mjs'], { cwd: root });
101
+ try {
102
+ const reads = [];
103
+ const result = await scanCensus({
104
+ repoRoot: root,
105
+ enabled: true,
106
+ hasActive: true,
107
+ readText: async (path) => {
108
+ reads.push(path);
109
+ return (await import('node:fs/promises')).readFile(path, 'utf8');
110
+ },
111
+ });
112
+
113
+ assert.ok(!reads.includes(join(root, 'src', 'external.mjs')));
114
+ assert.ok(!reads.includes(externalPath));
115
+ assert.ok(!serializeCensus(result).includes('CENSUS_EXTERNAL_SECRET_CANARY'));
116
+ assert.ok(!result.denominator.some(({ path }) => path === 'src/external.mjs'));
117
+ assert.deepEqual(result.families.surfaces, [
118
+ { name: 'production-config', status: 'abgedeckt', type: 'surface' },
119
+ { name: 'src', status: 'offen', type: 'surface' },
120
+ ]);
121
+ assert.equal(result.state, 'refresh_required');
122
+ } finally {
123
+ await rm(root, { recursive: true, force: true });
124
+ await rm(external, { recursive: true, force: true });
125
+ }
126
+ });
127
+
128
+ test('a tracked product file missing from the working tree forces an open refresh', async () => {
129
+ const root = await makeRepository();
130
+ try {
131
+ await rm(join(root, 'src', 'index.mjs'));
132
+
133
+ const result = await scanCensus({ repoRoot: root, enabled: true, hasActive: true });
134
+
135
+ assert.deepEqual(result.denominator.map(({ path }) => path), ['package.json']);
136
+ assert.deepEqual(result.families.surfaces, [
137
+ { name: 'production-config', status: 'abgedeckt', type: 'surface' },
138
+ { name: 'src', status: 'offen', type: 'surface' },
139
+ ]);
140
+ assert.equal(result.state, 'refresh_required');
141
+ } finally {
142
+ await rm(root, { recursive: true, force: true });
143
+ }
144
+ });
145
+
146
+ test('an unreadable tracked product file makes its family open without a duplicate covered verdict', async () => {
147
+ const root = await makeRepository();
148
+ try {
149
+ const result = await scanCensus({
150
+ repoRoot: root,
151
+ enabled: true,
152
+ hasActive: true,
153
+ readText: async (path) => {
154
+ if (path === join(root, 'src', 'index.mjs')) throw new Error('simulated unreadable product file');
155
+ return (await import('node:fs/promises')).readFile(path, 'utf8');
156
+ },
157
+ });
158
+
159
+ assert.deepEqual(result.families.surfaces, [
160
+ { name: 'production-config', status: 'abgedeckt', type: 'surface' },
161
+ { name: 'src', status: 'offen', type: 'surface' },
162
+ ]);
163
+ assert.equal(result.state, 'refresh_required');
164
+ } finally {
165
+ await rm(root, { recursive: true, force: true });
166
+ }
167
+ });
168
+
169
+ test('an untracked source root is open and prevents current', async () => {
170
+ const root = await makeRepository();
171
+ try {
172
+ await mkdir(join(root, 'new-service', 'src'), { recursive: true });
173
+ await writeFile(join(root, 'new-service', 'src', 'index.mjs'), 'export const newService = true;\n');
174
+
175
+ const result = await scanCensus({ repoRoot: root, enabled: true, hasActive: true });
176
+
177
+ assert.equal(result.state, 'refresh_required');
178
+ assert.deepEqual(result.families.surfaces.filter(({ status }) => status === 'offen'), [
179
+ { name: 'new-service/src', status: 'offen', type: 'surface' },
180
+ ]);
181
+ } finally {
182
+ await rm(root, { recursive: true, force: true });
183
+ }
184
+ });
185
+
186
+ test('delta reports compact path changes and open families', async () => {
187
+ const root = await makeRepository();
188
+ try {
189
+ const previous = await scanCensus({ repoRoot: root });
190
+ await writeFile(join(root, 'src', 'index.mjs'), 'export const answer = 43;\n');
191
+ await mkdir(join(root, 'new-service', 'src'), { recursive: true });
192
+ await writeFile(join(root, 'new-service', 'src', 'index.mjs'), 'export const newService = true;\n');
193
+ const next = await scanCensus({ repoRoot: root });
194
+
195
+ assert.deepEqual(diffCensus(previous, next), {
196
+ added: [],
197
+ changed: ['src/index.mjs'],
198
+ open: ['new-service/src'],
199
+ removed: [],
200
+ });
201
+ } finally {
202
+ await rm(root, { recursive: true, force: true });
203
+ }
204
+ });
205
+
206
+ test('content-only edits do not change the topology fingerprint', async () => {
207
+ const root = await makeRepository();
208
+ try {
209
+ const previous = await scanCensus({ repoRoot: root });
210
+ await writeFile(join(root, 'src', 'index.mjs'), 'export const answer = 99;\n');
211
+ const next = await scanCensus({ repoRoot: root });
212
+
213
+ assert.equal(previous.fingerprints.topology, next.fingerprints.topology);
214
+ assert.deepEqual(diffCensus(previous, next).changed, ['src/index.mjs']);
215
+ } finally {
216
+ await rm(root, { recursive: true, force: true });
217
+ }
218
+ });
219
+
220
+ test('family verdicts expose every machine-readable contract value', () => {
221
+ assert.deepEqual(CENSUS_VERDICTS, {
222
+ covered: 'abgedeckt',
223
+ notRelevant: 'nicht relevant',
224
+ open: 'offen',
225
+ });
226
+ });
227
+
228
+ test('behavior families represent covered, not-relevant, and open verdicts separately', async () => {
229
+ const root = await makeRepository();
230
+ try {
231
+ const result = await scanCensus({
232
+ repoRoot: root,
233
+ enabled: true,
234
+ hasActive: true,
235
+ behaviorFamilies: [
236
+ { name: 'auth-session', status: CENSUS_VERDICTS.covered },
237
+ { name: 'billing', status: CENSUS_VERDICTS.notRelevant },
238
+ { name: 'email-delivery', status: CENSUS_VERDICTS.open },
239
+ ],
240
+ });
241
+
242
+ assert.deepEqual(result.families.behaviors, [
243
+ { name: 'auth-session', status: 'abgedeckt', type: 'behavior' },
244
+ { name: 'billing', status: 'nicht relevant', type: 'behavior' },
245
+ { name: 'email-delivery', status: 'offen', type: 'behavior' },
246
+ ]);
247
+ assert.ok(result.families.surfaces.every(({ type }) => type === 'surface'));
248
+ assert.equal(result.state, 'refresh_required');
249
+ } finally {
250
+ await rm(root, { recursive: true, force: true });
251
+ }
252
+ });
@@ -0,0 +1,14 @@
1
+ function byPath(census) {
2
+ return new Map(census.denominator.map((entry) => [entry.path, entry.hash]));
3
+ }
4
+
5
+ export function diffCensus(previous, next) {
6
+ const before = byPath(previous);
7
+ const after = byPath(next);
8
+ const added = [...after.keys()].filter((path) => !before.has(path)).sort();
9
+ const removed = [...before.keys()].filter((path) => !after.has(path)).sort();
10
+ const changed = [...after.keys()].filter((path) => before.has(path) && before.get(path) !== after.get(path)).sort();
11
+ const open = [...next.families.surfaces, ...next.families.behaviors]
12
+ .filter(({ status }) => status === 'offen').map(({ name }) => name).sort();
13
+ return { added, changed, open, removed };
14
+ }
@@ -0,0 +1,20 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ export const CENSUS_BUILDER_VERSION = '1';
4
+
5
+ export function sha256(value) {
6
+ return createHash('sha256').update(value).digest('hex');
7
+ }
8
+
9
+ export function fingerprintCensus({ denominator, evidence, families }) {
10
+ const structural = (entries) => entries.map(({ family, kind, path }) => ({ family, kind, path }));
11
+ const topology = {
12
+ denominator: structural(denominator),
13
+ evidence: structural(evidence),
14
+ families,
15
+ };
16
+ return {
17
+ builder: sha256(`agent-workflow-kit-census:${CENSUS_BUILDER_VERSION}`),
18
+ topology: sha256(JSON.stringify(topology)),
19
+ };
20
+ }
@@ -0,0 +1,9 @@
1
+ export { scanCensus } from './scan.mjs';
2
+ export { CENSUS_BUILDER_VERSION, fingerprintCensus } from './fingerprint.mjs';
3
+ export { CENSUS_STATES, CENSUS_VERDICTS, resolveCensusState } from './state.mjs';
4
+ export { diffCensus } from './delta.mjs';
5
+ export { activateCensus, CensusTransactionError } from './transaction.mjs';
6
+
7
+ export function serializeCensus(census) {
8
+ return `${JSON.stringify(census)}\n`;
9
+ }