@ikon85/agent-workflow-kit 0.16.3 → 0.17.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.
package/README.md CHANGED
@@ -332,6 +332,15 @@ still reference. Flags: `--force` (overwrite pre-existing files on `init`),
332
332
 
333
333
  ## Release notes
334
334
 
335
+ ### 0.17.0
336
+
337
+ - added: `src/lib/release-preview.mjs`
338
+ - added: `src/lib/semver.mjs`
339
+
340
+ ### 0.16.4
341
+
342
+ - Metadata-only release.
343
+
335
344
  ### 0.16.3
336
345
 
337
346
  - changed: `.agents/skills/grill-me/SKILL.md`
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitVersion": "0.16.3",
2
+ "kitVersion": "0.17.0",
3
3
  "files": [
4
4
  {
5
5
  "path": ".agents/skills/ask-matt/SKILL.md",
@@ -2069,6 +2069,20 @@
2069
2069
  "sha256": "24ce9373c464cc8363c53b0c690c3231cbf11362e6e08336e0e97663579d1f19",
2070
2070
  "mode": 493,
2071
2071
  "origin": "kit"
2072
+ },
2073
+ {
2074
+ "path": "src/lib/release-preview.mjs",
2075
+ "kind": "script",
2076
+ "sha256": "a4e6627fac2d62812e44cfcb80c46539563734e7fa11f94b3dda37ff6fe6ae8f",
2077
+ "mode": 420,
2078
+ "origin": "kit"
2079
+ },
2080
+ {
2081
+ "path": "src/lib/semver.mjs",
2082
+ "kind": "script",
2083
+ "sha256": "7d0a8da3740ed1eb79edb80f0eedb3cd1de47692d4886c6b119b39073a6468d1",
2084
+ "mode": 420,
2085
+ "origin": "kit"
2072
2086
  }
2073
2087
  ]
2074
2088
  }
@@ -0,0 +1,128 @@
1
+ # Wave 43 script and hook census
2
+
3
+ Verified on 2026-07-15 against `agent-workflow-kit` commit
4
+ `2d47dce23d9204b44951334c3c370bdb997c0943` and Testreporter commit
5
+ [`458747ebe8d67f66840cb3c1e75284cf2be7fb20`](https://github.com/iKon85/Testreporter/tree/458747ebe8d67f66840cb3c1e75284cf2be7fb20).
6
+ This is the research input for issue #59. It records facts only; it does not make
7
+ ship, opt-in, profile-seam, or maintenance decisions.
8
+
9
+ ## Method and reconciliation
10
+
11
+ The public-repository denominator is every tracked file below `scripts/**` and
12
+ `.claude/hooks/**`, excluding `test_*.py`, `*.test.mjs`, and the metadata file
13
+ `scripts/.npmignore`. The recount is:
14
+
15
+ ```text
16
+ 68 tracked entries
17
+ - 31 tests
18
+ - 1 metadata file
19
+ = 36 current script/hook entries
20
+ = 32 scripts + 4 hooks
21
+ = 30 shipped helpers + 6 maintainer-only helpers
22
+ ```
23
+
24
+ The shipped/helper split is derived from
25
+ [`src/lib/bundle.mjs`](../../src/lib/bundle.mjs), whose `HELPER_FILES` list is
26
+ the public distribution authority. The historical input is the 26-candidate
27
+ list in [Testreporter #1976](https://github.com/iKon85/Testreporter/issues/1976),
28
+ originally reconciled as 11 portable plus 15 generalize-first candidates in
29
+ [Testreporter #1964](https://github.com/iKon85/Testreporter/issues/1964).
30
+
31
+ Fresh exact-path comparison:
32
+
33
+ ```text
34
+ Historical candidates: 26/26 accounted for
35
+ Still present at the exact Testreporter path: 26/26
36
+ Present at the exact public-repo path: 0/26
37
+ Proven exact rename in the public repo: 0/26
38
+ Explicitly retired by a locked decision: 0/26
39
+ ```
40
+
41
+ Therefore every historical row is classified as **present in the current
42
+ consumer and absent from the current public denominator**. Related public
43
+ primitives are called out only as inputs to the later grill; similarity is not
44
+ treated as proof of a rename or as a ship decision.
45
+
46
+ ## Fresh public denominator: 36 of 36
47
+
48
+ | Current path | Distribution status |
49
+ |---|---|
50
+ | `.claude/hooks/_hook_utils.py` | shipped helper |
51
+ | `.claude/hooks/drift-guard.py` | shipped helper |
52
+ | `.claude/hooks/skill-drift-hint.py` | shipped helper |
53
+ | `.claude/hooks/sync-board-status.py` | shipped helper |
54
+ | `scripts/anchor_table.py` | shipped helper |
55
+ | `scripts/board-sync.py` | shipped helper |
56
+ | `scripts/board_config.py` | shipped helper |
57
+ | `scripts/board_fields.py` | shipped helper |
58
+ | `scripts/build-kit.mjs` | maintainer-only |
59
+ | `scripts/census/delta.mjs` | shipped helper |
60
+ | `scripts/census/fingerprint.mjs` | shipped helper |
61
+ | `scripts/census/index.mjs` | shipped helper |
62
+ | `scripts/census/scan.mjs` | shipped helper |
63
+ | `scripts/census/state.mjs` | shipped helper |
64
+ | `scripts/census/transaction.mjs` | shipped helper |
65
+ | `scripts/check-kit-staleness.mjs` | maintainer-only |
66
+ | `scripts/execute-ready-check.py` | shipped helper |
67
+ | `scripts/grill-census-wiring-guard.mjs` | maintainer-only |
68
+ | `scripts/issue_deps.py` | shipped helper |
69
+ | `scripts/kit-release.mjs` | shipped helper |
70
+ | `scripts/kit-update-pr.mjs` | shipped helper |
71
+ | `scripts/lib/audit-refs.mjs` | maintainer-only |
72
+ | `scripts/lib/scrub.mjs` | maintainer-only |
73
+ | `scripts/loc_offender_core.py` | shipped helper |
74
+ | `scripts/loc_offender_gate.py` | shipped helper |
75
+ | `scripts/node_kind.py` | shipped helper |
76
+ | `scripts/portability_profile_scan.py` | maintainer-only |
77
+ | `scripts/pr-body-check.py` | shipped helper |
78
+ | `scripts/program_graph.py` | shipped helper |
79
+ | `scripts/program_graph_parse.py` | shipped helper |
80
+ | `scripts/program_graph_validate.py` | shipped helper |
81
+ | `scripts/program_sync.py` | shipped helper |
82
+ | `scripts/release-delta-guard.mjs` | shipped helper |
83
+ | `scripts/release-parity.mjs` | shipped helper |
84
+ | `scripts/release-state.mjs` | shipped helper |
85
+ | `scripts/wrapup-land.py` | shipped helper |
86
+
87
+ ## Historical candidate crosswalk: 26 of 26
88
+
89
+ All Testreporter links below are pinned to the verified consumer commit. “No
90
+ dedicated tracked test found” means the fresh tracked-tree search found no test
91
+ named for that candidate; it is not a statement about runtime correctness.
92
+
93
+ | Historical candidate | Current public status and adjacent evidence | Current consumer dependency | Existing consumer test evidence |
94
+ |---|---|---|---|
95
+ | `scripts/audit_gate.py` | Exact path absent. | [`package.json#L44`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/package.json#L44) and `.githooks/pre-push` invoke it. | [`scripts/test_audit_gate.py`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/scripts/test_audit_gate.py), [`scripts/test_pre_push_audit_gate.py`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/scripts/test_pre_push_audit_gate.py) |
96
+ | `scripts/cleanup-worktrees.sh` | Exact path absent. Public `scripts/wrapup-land.py` has adjacent worktree-cleanup behavior, not a proven rename. | No separate tracked consumer found beyond direct/manual use. | No dedicated tracked test found. |
97
+ | `scripts/install-git-hooks.sh` | Exact path absent. | [`package.json#L45`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/package.json#L45) invokes it during postinstall. | [`scripts/test_install_git_hooks.py`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/scripts/test_install_git_hooks.py) |
98
+ | `scripts/ensure-gitleaks.sh` | Exact path absent. | [`package.json#L45`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/package.json#L45) invokes it during postinstall; `docs/security/secret-scan.md` documents it. | No dedicated tracked test found. |
99
+ | `.claude/hooks/block-bg-double-background.py` | Exact path absent. | [`.claude/settings.json#L85`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/settings.json#L85) wires it. | [`.claude/hooks/test_block_bg_double_background.py`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/hooks/test_block_bg_double_background.py) |
100
+ | `.claude/hooks/block-npm-install-in-pnpm.py` | Exact path absent. | [`.claude/settings.json#L75`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/settings.json#L75) wires it. | [`.claude/hooks/test_block_npm_install_in_pnpm.py`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/hooks/test_block_npm_install_in_pnpm.py) |
101
+ | `.claude/hooks/block-secrets.py` | Exact path absent. | [`.claude/settings.json#L56-L65`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/settings.json#L56-L65) wires it at two hook events. | [`.claude/hooks/test_block_secrets_smoke.py`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/hooks/test_block_secrets_smoke.py) |
102
+ | `.claude/hooks/branch-watch.py` | Exact path absent. | [`.claude/settings.json#L161`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/settings.json#L161) wires it. | [`.claude/hooks/test_branch_watch_smoke.py`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/hooks/test_branch_watch_smoke.py) |
103
+ | `.claude/hooks/grep-shim-guard.py` | Exact path absent. | [`.claude/settings.json#L80`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/settings.json#L80) wires it. | [`.claude/hooks/test_grep_shim_guard.py`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/hooks/test_grep_shim_guard.py) |
104
+ | `.claude/hooks/recon-size-hint.py` | Exact path absent. | [`.claude/settings.json#L134`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/settings.json#L134) wires it. | [`.claude/hooks/test_recon_size_hint.py`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/hooks/test_recon_size_hint.py) |
105
+ | `.claude/hooks/loc-offender-forewarn.py` | Exact path absent. Public `scripts/loc_offender_core.py` and `scripts/loc_offender_gate.py` are related shipped primitives, not a proven rename. | [`.claude/settings.json#L18`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/settings.json#L18) wires it. | [`scripts/test_loc_offender_forewarn.py`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/scripts/test_loc_offender_forewarn.py) |
106
+ | `scripts/bump-version.sh` | Exact path absent. Public `scripts/kit-release.mjs` contains tested SemVer and metadata-update behavior, not a proven rename. | No separate consumer wiring found; the script is directly invoked. | No dedicated tracked test found for the historical script. |
107
+ | `scripts/consolidate-memories.sh` | Exact path absent. | The two historical memory templates name it as their consolidator. | No dedicated tracked test found. |
108
+ | `scripts/memory-templates/meta_decision_layer_choice.md` | Exact path absent. | Input consumed by `scripts/consolidate-memories.sh`. | No dedicated tracked test found. |
109
+ | `scripts/memory-templates/meta_memory_lifecycle.md` | Exact path absent. | Input consumed by `scripts/consolidate-memories.sh`. | No dedicated tracked test found. |
110
+ | `scripts/setup-worktree.sh` | Exact path absent. Public workflow prose allows a project helper or plain `git worktree add`; that is not a rename. | Central Testreporter worktree, port, and local-env setup primitive referenced by workflow documentation. | [`scripts/setup-worktree.test.mjs`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/scripts/setup-worktree.test.mjs), [`scripts/test_pre_push_loc_gate.py`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/scripts/test_pre_push_loc_gate.py) |
111
+ | `.claude/hooks/baseline-capture-hint.py` | Exact path absent. | [`.claude/settings.json#L114`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/settings.json#L114) wires it. | [`.claude/hooks/test_baseline_capture_hint_smoke.py`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/hooks/test_baseline_capture_hint_smoke.py) |
112
+ | `.claude/hooks/branch-context.py` | Exact path absent. | [`.claude/settings.json#L13`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/settings.json#L13) wires it. | [`.claude/hooks/test_branch_context_smoke.py`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/hooks/test_branch_context_smoke.py) |
113
+ | `.claude/hooks/convention-drift-hint.py` | Exact path absent. Public `.claude/hooks/skill-drift-hint.py` and `drift-guard.py` are narrower adjacent mechanisms, not proven renames. | [`.claude/settings.json#L33`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/settings.json#L33) and rule `SOURCES.txt` files refer to it. | No dedicated tracked test found. |
114
+ | `.claude/hooks/enforce-worktree.py` | Exact path absent. | [`.claude/settings.json#L109`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/settings.json#L109) wires it. | [`.claude/hooks/test_enforce_worktree.py`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/hooks/test_enforce_worktree.py) |
115
+ | `.claude/hooks/enforce-worktree-cwd.py` | Exact path absent. | [`.claude/settings.json#L95`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/settings.json#L95) wires it. | [`.claude/hooks/test_enforce_worktree_cwd_smoke.py`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/hooks/test_enforce_worktree_cwd_smoke.py) |
116
+ | `.claude/hooks/enforce-worktree-discipline.py` | Exact path absent. | [`.claude/settings.json#L70`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/settings.json#L70) wires it. | [`.claude/hooks/test_enforce_worktree_discipline_smoke.py`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/hooks/test_enforce_worktree_discipline_smoke.py) |
117
+ | `.claude/hooks/migration-snapshot-reminder.py` | Exact path absent. | [`.claude/settings.json#L166`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/settings.json#L166) wires it. | [`.claude/hooks/test_migration_snapshot_reminder_smoke.py`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/hooks/test_migration_snapshot_reminder_smoke.py) |
118
+ | `.claude/hooks/pre-refactor-sweep.py` | Exact path absent. | [`.claude/settings.json#L23`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/settings.json#L23) wires it. | [`.claude/hooks/test_pre_refactor_sweep.py`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/hooks/test_pre_refactor_sweep.py) |
119
+ | `.claude/hooks/typecheck-on-stop.sh` | Exact path absent. | [`.claude/settings.json#L145`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/settings.json#L145) wires it. | No dedicated tracked test found. |
120
+ | `.claude/hooks/slice-handoff-hint.py` | Exact path absent. | [`.claude/settings.json#L44`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/settings.json#L44) wires it. | [`.claude/hooks/test_slice_handoff_hint_smoke.py`](https://github.com/iKon85/Testreporter/blob/458747ebe8d67f66840cb3c1e75284cf2be7fb20/.claude/hooks/test_slice_handoff_hint_smoke.py) |
121
+
122
+ ## Inputs for the decision grill
123
+
124
+ The crosswalk establishes the decision set but intentionally leaves its verdict
125
+ columns blank. Issue #59 must decide, with the user, each row’s outcome, opt-in,
126
+ profile seam, security and maintenance cost, and ship verdict. In particular,
127
+ the adjacent public primitives identified above must be compared behaviorally
128
+ before any row is called replaced, redundant, or already shipped.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikon85/agent-workflow-kit",
3
- "version": "0.16.3",
3
+ "version": "0.17.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": {
@@ -9,6 +9,9 @@ import { execFileSync } from 'node:child_process';
9
9
  import { buildKit } from './build-kit.mjs';
10
10
 
11
11
  const REPO = join(dirname(fileURLToPath(import.meta.url)), '..');
12
+ const PUBLIC_CENSUS_UNIT = JSON.parse(await readFile(
13
+ join(REPO, 'test/fixtures/census-consumers/public-unit.json'), 'utf8',
14
+ )).paths;
12
15
  async function withBuild(fn) {
13
16
  const dist = await mkdtemp(join(tmpdir(), 'awkit-build-'));
14
17
  try { return await fn(dist, await buildKit({ repoRoot: REPO, distDir: dist })); }
@@ -43,6 +46,19 @@ test('current build contains post-tag public files and repository metadata', asy
43
46
  });
44
47
  });
45
48
 
49
+ test('current build contains the complete dual-surface census consumer unit', async () => {
50
+ await withBuild(async (dist, report) => {
51
+ const manifest = JSON.parse(await readFile(join(dist, 'agent-workflow-kit.package.json'), 'utf8'));
52
+ const paths = new Set(manifest.files.map(({ path }) => path));
53
+ assert.deepEqual(PUBLIC_CENSUS_UNIT.filter((path) => !paths.has(path)), []);
54
+ assert.equal(report.fileCount, manifest.files.length);
55
+ assert.equal(
56
+ await readFile(join(dist, '.claude/skills/census-update/SKILL.md'), 'utf8'),
57
+ await readFile(join(dist, '.agents/skills/census-update/SKILL.md'), 'utf8'),
58
+ );
59
+ });
60
+ });
61
+
46
62
  test('current build is self-contained and never reaches into a consumer checkout', async () => {
47
63
  const source = await readFile(join(REPO, 'scripts/build-kit.mjs'), 'utf8');
48
64
  assert.doesNotMatch(source, /testreporter|tools\/agent-workflow-kit/);
@@ -67,6 +83,9 @@ test('npm pack keeps product files but excludes runtime residue', async () => {
67
83
  assert.ok(files.includes('scripts/kit-update-pr.mjs'));
68
84
  assert.ok(files.includes('.claude/hooks/drift-guard.py'));
69
85
  assert.ok(files.includes('.claude/skills/tdd/SKILL.md'));
86
+ for (const path of PUBLIC_CENSUS_UNIT) {
87
+ assert.ok(files.includes(path), `pack missing ${path}`);
88
+ }
70
89
  assert.ok(files.every((path) => !path.startsWith('.claude/logs/')));
71
90
  assert.ok(files.every((path) => !path.includes('__pycache__') && !path.endsWith('.pyc')));
72
91
  const pkg = JSON.parse(execFileSync('node', ['-p', 'JSON.stringify(require("./package.json"))'], {
@@ -0,0 +1,227 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import { cp, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
6
+ import { tmpdir } from 'node:os';
7
+ import { join } from 'node:path';
8
+ import { fileURLToPath, pathToFileURL } from 'node:url';
9
+ import {
10
+ CENSUS_BUILDER_VERSION,
11
+ activateCensus,
12
+ diffCensus,
13
+ scanCensus,
14
+ } from './census/index.mjs';
15
+
16
+ const exec = promisify(execFile);
17
+ const FIXTURES = new URL('../test/fixtures/census-consumers/', import.meta.url);
18
+ const REPO = fileURLToPath(new URL('../', import.meta.url));
19
+
20
+ async function consumer(name, tracked = ['.']) {
21
+ const root = await mkdtemp(join(tmpdir(), `awk-census-${name}-`));
22
+ await cp(new URL(`${name}/`, FIXTURES), root, { recursive: true });
23
+ await exec('git', ['init', '--quiet'], { cwd: root });
24
+ await exec('git', ['add', ...tracked], { cwd: root });
25
+ return root;
26
+ }
27
+
28
+ async function cleanup(root) {
29
+ await rm(root, { recursive: true, force: true });
30
+ }
31
+
32
+ async function json(path) {
33
+ return JSON.parse(await readFile(path, 'utf8'));
34
+ }
35
+
36
+ async function backstopStatus(root) {
37
+ const { stdout } = await exec('python3', [
38
+ join(REPO, '.claude/hooks/drift-guard.py'), '--census-status',
39
+ ], { cwd: root });
40
+ return JSON.parse(stdout);
41
+ }
42
+
43
+ test('brownfield reports real X of Y surfaces separately from behaviors', async () => {
44
+ const root = await consumer('brownfield');
45
+ try {
46
+ const profile = await json(join(root, '.census/profile.json'));
47
+ const census = await scanCensus({
48
+ repoRoot: root,
49
+ enabled: profile.enabled,
50
+ hasActive: true,
51
+ behaviorFamilies: profile.decisions.map(({ family, status }) => ({ name: family, status })),
52
+ });
53
+ const covered = census.families.surfaces.filter(({ status }) => status === 'abgedeckt').length;
54
+
55
+ assert.equal(census.state, 'current');
56
+ assert.equal(`${covered} of ${census.families.surfaces.length}`, '3 of 3');
57
+ assert.deepEqual(census.families.behaviors, [
58
+ { name: 'authentication', status: 'abgedeckt', type: 'behavior' },
59
+ { name: 'email-delivery', status: 'nicht relevant', type: 'behavior' },
60
+ ]);
61
+ assert.ok(census.families.surfaces.every(({ type }) => type === 'surface'));
62
+ } finally { await cleanup(root); }
63
+ });
64
+
65
+ test('greenfield without an active snapshot remains honest bootstrap', async () => {
66
+ const root = await consumer('greenfield');
67
+ try {
68
+ const census = await scanCensus({ repoRoot: root, enabled: true, hasActive: false });
69
+ assert.equal(census.state, 'bootstrap');
70
+ assert.ok(census.denominator.length > 0, 'bootstrap still reports discovered facts');
71
+ } finally { await cleanup(root); }
72
+ });
73
+
74
+ test('unknown pattern stays open until its repository-local scanner test passes', async () => {
75
+ const root = await consumer('unknown-pattern', ['package.json', 'src', '.census']);
76
+ try {
77
+ const activePath = join(root, '.census/active.json');
78
+ const profilePath = join(root, '.census/profile.json');
79
+ const surface = 'services/payments/src';
80
+ const scannerRecord = {
81
+ surface,
82
+ module: 'scripts/census-local/scan-services.mjs',
83
+ export: 'scanServices',
84
+ test: 'test/census-services.test.mjs',
85
+ };
86
+
87
+ const untracked = await scanCensus({ repoRoot: root, enabled: true, hasActive: true });
88
+ assert.equal(untracked.state, 'refresh_required');
89
+ assert.deepEqual(diffCensus(untracked, untracked).open, [surface]);
90
+
91
+ // Establish real active history before the repository grows the unknown surface.
92
+ await rm(join(root, 'services'), { recursive: true });
93
+ await rm(join(root, 'scripts'), { recursive: true });
94
+ await rm(join(root, 'test'), { recursive: true });
95
+ const known = await scanCensus({ repoRoot: root, enabled: true, hasActive: true });
96
+ await writeFile(activePath, `${JSON.stringify({
97
+ ...known,
98
+ profileReport: { decisions: [], localScanners: [], overrides: [] },
99
+ })}\n`);
100
+
101
+ await cp(new URL('unknown-pattern/services/', FIXTURES), join(root, 'services'), { recursive: true });
102
+ await exec('git', ['add', 'services'], { cwd: root });
103
+ const trackedOnly = await backstopStatus(root);
104
+ assert.equal(trackedOnly.state, 'refresh_required');
105
+ assert.ok(trackedOnly.reasons.includes('topology'));
106
+
107
+ const profile = await json(profilePath);
108
+ profile.localScanners.push(scannerRecord);
109
+ await writeFile(profilePath, `${JSON.stringify(profile)}\n`);
110
+ const missingProof = await backstopStatus(root);
111
+ assert.equal(missingProof.state, 'refresh_required');
112
+ assert.ok(missingProof.reasons.includes(`proof:${surface}`));
113
+
114
+ await cp(new URL('unknown-pattern/scripts/', FIXTURES), join(root, 'scripts'), { recursive: true });
115
+ await cp(new URL('unknown-pattern/test/', FIXTURES), join(root, 'test'), { recursive: true });
116
+ await writeFile(
117
+ join(root, scannerRecord.test),
118
+ "import { test } from 'node:test'; test('failing local proof', () => { throw new Error('no'); });\n",
119
+ );
120
+ await exec('git', ['add', 'scripts', 'test', '.census/profile.json'], { cwd: root });
121
+ const failedProof = await backstopStatus(root);
122
+ assert.equal(failedProof.state, 'refresh_required');
123
+ assert.ok(failedProof.reasons.includes(`proof:${surface}`));
124
+
125
+ await cp(
126
+ new URL('unknown-pattern/test/census-services.test.mjs', FIXTURES),
127
+ join(root, scannerRecord.test),
128
+ );
129
+ const candidate = await scanCensus({ repoRoot: root, enabled: true, hasActive: true });
130
+ assert.equal(candidate.state, 'current');
131
+ await activateCensus({
132
+ activePath,
133
+ candidate: {
134
+ ...candidate,
135
+ profileReport: { decisions: [], localScanners: [scannerRecord], overrides: [] },
136
+ },
137
+ verify: async () => {
138
+ await exec('node', ['--test', scannerRecord.test], { cwd: root });
139
+ const scanner = await import(`${pathToFileURL(join(root, scannerRecord.module)).href}?proof=${Date.now()}`);
140
+ return scanner[scannerRecord.export]().includes(surface);
141
+ },
142
+ });
143
+
144
+ const proven = await backstopStatus(root);
145
+ assert.equal(proven.state, 'current');
146
+ assert.deepEqual(proven.reasons, []);
147
+ const active = await json(activePath);
148
+ assert.equal(active.profileReport.localScanners[0].test, scannerRecord.test);
149
+ } finally { await cleanup(root); }
150
+ });
151
+
152
+ test('scan and compact delta do not overwrite a consumer-local modification', async () => {
153
+ const root = await consumer('local-modified');
154
+ try {
155
+ const sourcePath = join(root, 'src/index.mjs');
156
+ const activePath = join(root, '.census/active.json');
157
+ const sourceBefore = await readFile(sourcePath);
158
+ const activeBefore = await readFile(activePath);
159
+ const previous = { denominator: [], families: { surfaces: [], behaviors: [] } };
160
+ const current = await scanCensus({ repoRoot: root, enabled: true, hasActive: true });
161
+ const delta = diffCensus(previous, current);
162
+
163
+ assert.deepEqual(Object.keys(delta), ['added', 'changed', 'open', 'removed']);
164
+ assert.ok(JSON.stringify(delta).length < JSON.stringify(current).length);
165
+ assert.deepEqual(await readFile(sourcePath), sourceBefore);
166
+ assert.deepEqual(await readFile(activePath), activeBefore);
167
+ } finally { await cleanup(root); }
168
+ });
169
+
170
+ test('secret canary is absent from scan, delta, and file reads', async () => {
171
+ const root = await consumer('secret-canary');
172
+ try {
173
+ const reads = [];
174
+ const scan = await scanCensus({
175
+ repoRoot: root,
176
+ readText: async (path) => {
177
+ reads.push(path);
178
+ return readFile(path, 'utf8');
179
+ },
180
+ });
181
+ const output = JSON.stringify({ scan, delta: diffCensus({ denominator: [] }, scan) });
182
+ assert.doesNotMatch(output, /CENSUS_SECRET_CANARY/);
183
+ assert.ok(reads.every((path) => !path.includes('credentials')));
184
+ } finally { await cleanup(root); }
185
+ });
186
+
187
+ test('interrupted activation preserves the active census byte-for-byte', async () => {
188
+ const root = await consumer('interrupted');
189
+ try {
190
+ const activePath = join(root, '.census/active.json');
191
+ const before = await readFile(activePath);
192
+ const candidate = await scanCensus({ repoRoot: root, enabled: true, hasActive: true });
193
+ await assert.rejects(activateCensus({
194
+ activePath,
195
+ candidate,
196
+ verify: async () => true,
197
+ renameCandidate: async () => { throw new Error('fixture interruption'); },
198
+ }), (error) => error.state === 'failed');
199
+ assert.deepEqual(await readFile(activePath), before);
200
+ } finally { await cleanup(root); }
201
+ });
202
+
203
+ test('consumer update reports a newer builder without silently mutating its census', async () => {
204
+ const root = await consumer('consumer-update');
205
+ try {
206
+ const activePath = join(root, '.census/active.json');
207
+ const profilePath = join(root, '.census/profile.json');
208
+ const fresh = await scanCensus({ repoRoot: root, enabled: true, hasActive: true });
209
+ await writeFile(activePath, `${JSON.stringify({
210
+ ...fresh,
211
+ fingerprints: { ...fresh.fingerprints, builder: 'older-builder-fingerprint' },
212
+ profileReport: { decisions: [], localScanners: [], overrides: [] },
213
+ })}\n`);
214
+ const activeBefore = await readFile(activePath);
215
+ const profileBefore = await readFile(profilePath);
216
+ const { stdout } = await exec('python3', [
217
+ join(REPO, '.claude/hooks/drift-guard.py'), '--census-status',
218
+ ], { cwd: root });
219
+ const status = JSON.parse(stdout);
220
+
221
+ assert.equal(status.state, 'refresh_required');
222
+ assert.equal(status.detail, `builder ${CENSUS_BUILDER_VERSION}`);
223
+ assert.ok(status.reasons.includes('builder'));
224
+ assert.deepEqual(await readFile(activePath), activeBefore);
225
+ assert.deepEqual(await readFile(profilePath), profileBefore);
226
+ } finally { await cleanup(root); }
227
+ });
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env python3
2
+ """Forward proof: a clean public checkout builds and serves census consumers."""
3
+ import json
4
+ import shutil
5
+ import subprocess
6
+ import tempfile
7
+ import unittest
8
+ from pathlib import Path
9
+
10
+ REPO = Path(__file__).resolve().parent.parent
11
+ PUBLIC_UNIT_RELATIVE = Path("test/fixtures/census-consumers/public-unit.json")
12
+ GREENFIELD_RELATIVE = Path("test/fixtures/census-consumers/greenfield")
13
+
14
+
15
+ class CensusForwardContract(unittest.TestCase):
16
+ def setUp(self):
17
+ self.tmp = Path(tempfile.mkdtemp(prefix="awk-census-forward-"))
18
+ self.addCleanup(shutil.rmtree, self.tmp, ignore_errors=True)
19
+ self.public = self.tmp / "public-kit"
20
+ self.public.mkdir()
21
+ source_root = REPO
22
+ self.forbidden_source_root = str(source_root)
23
+ archive = subprocess.Popen(
24
+ ["git", "archive", "HEAD"], cwd=source_root, stdout=subprocess.PIPE
25
+ )
26
+ unpack = subprocess.run(
27
+ ["tar", "-x", "-C", str(self.public)], stdin=archive.stdout,
28
+ capture_output=True, timeout=30,
29
+ )
30
+ archive.stdout.close()
31
+ archive_rc = archive.wait(timeout=30)
32
+ self.assertEqual((archive_rc, unpack.returncode), (0, 0), unpack.stderr.decode())
33
+ # From this boundary onward every executable, fixture, and assertion input
34
+ # is resolved inside the archived public checkout.
35
+ self.assertTrue((self.public / GREENFIELD_RELATIVE).is_dir())
36
+ self.assertTrue((self.public / PUBLIC_UNIT_RELATIVE).is_file())
37
+ build = subprocess.run(
38
+ ["node", "scripts/build-kit.mjs"], cwd=self.public,
39
+ capture_output=True, text=True, timeout=120,
40
+ )
41
+ self.assertEqual(build.returncode, 0, build.stderr)
42
+ self.dist = self.public / "dist-kit"
43
+
44
+ def test_built_public_checkout_exercises_both_surfaces_and_scanner(self):
45
+ claude = (self.dist / ".claude/skills/census-update/SKILL.md").read_text()
46
+ codex = (self.dist / ".agents/skills/census-update/SKILL.md").read_text()
47
+ self.assertEqual(claude, codex)
48
+ for public_export in ("scanCensus", "diffCensus", "activateCensus"):
49
+ self.assertIn(public_export, claude)
50
+
51
+ consumer = self.tmp / "consumer"
52
+ fixture = self.public / GREENFIELD_RELATIVE
53
+ self.assertTrue(fixture.is_relative_to(self.public))
54
+ shutil.copytree(fixture, consumer)
55
+ subprocess.run(["git", "init", "--quiet"], cwd=consumer, check=True)
56
+ subprocess.run(["git", "add", "."], cwd=consumer, check=True)
57
+ runner = (
58
+ "import { scanCensus } from "
59
+ + json.dumps((self.dist / "scripts/census/index.mjs").as_uri())
60
+ + "; const result = await scanCensus({repoRoot: process.argv[1], enabled:true});"
61
+ + "console.log(JSON.stringify({state:result.state,count:result.denominator.length}));"
62
+ )
63
+ run = subprocess.run(
64
+ ["node", "--input-type=module", "--eval", runner, str(consumer)],
65
+ cwd=self.public, capture_output=True, text=True, timeout=30,
66
+ )
67
+ self.assertEqual(run.returncode, 0, run.stderr)
68
+ self.assertEqual(json.loads(run.stdout), {"state": "bootstrap", "count": 2})
69
+ self.assertNotIn("testreporter", run.stdout.lower())
70
+ self.assertNotIn(self.forbidden_source_root, runner)
71
+ self.assertNotIn(self.forbidden_source_root, run.stdout)
72
+
73
+ def test_public_pack_contains_complete_census_unit_without_foreign_reach(self):
74
+ pack = subprocess.run(
75
+ ["npm", "pack", "--dry-run", "--json"], cwd=self.public,
76
+ capture_output=True, text=True, timeout=120,
77
+ )
78
+ self.assertEqual(pack.returncode, 0, pack.stderr)
79
+ paths = {entry["path"] for entry in json.loads(pack.stdout)[0]["files"]}
80
+ expected = set(json.loads(
81
+ (self.public / PUBLIC_UNIT_RELATIVE).read_text(encoding="utf-8")
82
+ )["paths"])
83
+ self.assertEqual(expected - paths, set())
84
+ for path in expected:
85
+ text = (self.public / path).read_text(encoding="utf-8")
86
+ self.assertNotIn("tools/agent-workflow-kit", text)
87
+ self.assertNotIn("testreporter", text.lower())
88
+
89
+
90
+ if __name__ == "__main__":
91
+ unittest.main(verbosity=2)
@@ -194,6 +194,27 @@ class DistKitValidateGraphSmoke(unittest.TestCase):
194
194
  "language (fields.status.roles / --status-role) instead:\n"
195
195
  + "\n".join(hits))
196
196
 
197
+ def test_shipped_census_runs_for_a_foreign_greenfield_on_both_surfaces(self):
198
+ consumer = self.tmp / "census-consumer"
199
+ shutil.copytree(REPO / "test/fixtures/census-consumers/greenfield", consumer)
200
+ subprocess.run(["git", "init", "--quiet"], cwd=consumer, check=True)
201
+ subprocess.run(["git", "add", "."], cwd=consumer, check=True)
202
+ claude = (DIST / ".claude/skills/census-update/SKILL.md").read_text()
203
+ codex = (DIST / ".agents/skills/census-update/SKILL.md").read_text()
204
+ self.assertEqual(claude, codex)
205
+ module = (DIST / "scripts/census/index.mjs").as_uri()
206
+ runner = (
207
+ f'import {{ scanCensus }} from {json.dumps(module)};'
208
+ "const r=await scanCensus({repoRoot:process.argv[1],enabled:true});"
209
+ "console.log(JSON.stringify({state:r.state,count:r.denominator.length}));"
210
+ )
211
+ result = subprocess.run(
212
+ ["node", "--input-type=module", "--eval", runner, str(consumer)],
213
+ cwd=DIST, capture_output=True, text=True, timeout=30,
214
+ )
215
+ self.assertEqual(result.returncode, 0, result.stderr)
216
+ self.assertEqual(json.loads(result.stdout), {"state": "bootstrap", "count": 2})
217
+
197
218
 
198
219
  if __name__ == "__main__":
199
220
  unittest.main(verbosity=2)
@@ -36,6 +36,9 @@ import unittest
36
36
  from pathlib import Path
37
37
 
38
38
  REPO = Path(__file__).resolve().parent.parent
39
+ PUBLIC_CENSUS_UNIT = json.loads(
40
+ (REPO / "test/fixtures/census-consumers/public-unit.json").read_text(encoding="utf-8")
41
+ )["paths"]
39
42
  PUBLIC_SLUG = "iKon85/agent-workflow-kit"
40
43
  CREDIT_FILES = {"LICENSE", "README.md", "PROVENANCE.md", "THIRD-PARTY-NOTICES.md"}
41
44
  PRIVATE_SKILLS = {
@@ -295,6 +298,19 @@ class RealDistKitIsClean(unittest.TestCase):
295
298
  violations = audit_dir(REPO / "dist-kit")
296
299
  self.assertEqual(violations, [], "\n".join(violations[:40]))
297
300
 
301
+ def test_built_dist_kit_contains_publishable_census_without_foreign_reach(self):
302
+ build = subprocess.run(
303
+ ["node", str(REPO / "scripts/build-kit.mjs")], cwd=REPO,
304
+ capture_output=True, text=True, timeout=120,
305
+ )
306
+ self.assertEqual(build.returncode, 0, build.stderr)
307
+ for rel in PUBLIC_CENSUS_UNIT:
308
+ path = REPO / "dist-kit" / rel
309
+ self.assertTrue(path.is_file(), f"missing published census resource: {rel}")
310
+ text = path.read_text(encoding="utf-8")
311
+ self.assertNotIn("tools/agent-workflow-kit", text)
312
+ self.assertNotIn("testreporter", text.lower())
313
+
298
314
 
299
315
  if __name__ == "__main__":
300
316
  unittest.main(verbosity=2)
@@ -47,6 +47,10 @@ export const HELPER_FILES = [
47
47
  // the parity primitive rather than growing a second registry/GitHub comparison.
48
48
  { path: 'scripts/release-parity.mjs', kind: 'script', mode: 0o644 },
49
49
  { path: 'scripts/release-state.mjs', kind: 'script', mode: 0o644 },
50
+ // Consumer-owned project release profiles use these read-only shared
51
+ // primitives before any apply/commit/tag action is allowed.
52
+ { path: 'src/lib/semver.mjs', kind: 'script', mode: 0o644 },
53
+ { path: 'src/lib/release-preview.mjs', kind: 'script', mode: 0o644 },
50
54
  // GitHub-consumer automation: invokes the existing update command, then owns
51
55
  // only the stable tested branch/pull-request upsert.
52
56
  { path: 'scripts/kit-update-pr.mjs', kind: 'script', mode: 0o755 },
@@ -0,0 +1,114 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { nextVersion, parseSemver } from './semver.mjs';
5
+
6
+ export const PROJECT_RELEASE_PROFILE = 'docs/agents/workflow-capabilities.json';
7
+
8
+ export async function loadProjectReleaseProfile(consumerRoot) {
9
+ const profile = JSON.parse(
10
+ await readFile(join(consumerRoot, PROJECT_RELEASE_PROFILE), 'utf8'),
11
+ );
12
+ if (profile.schemaVersion !== 1 || !profile.projectRelease) {
13
+ throw new Error('project release profile must contain schemaVersion 1 and projectRelease');
14
+ }
15
+ if (!Array.isArray(profile.projectRelease.versionFiles)
16
+ || profile.projectRelease.versionFiles.length === 0) {
17
+ throw new Error('project release profile requires at least one versionFiles entry');
18
+ }
19
+ return profile.projectRelease;
20
+ }
21
+
22
+ async function readVersionFile(consumerRoot, path) {
23
+ const raw = await readFile(join(consumerRoot, path), 'utf8');
24
+ const body = JSON.parse(raw);
25
+ return {
26
+ path,
27
+ version: body.version,
28
+ sha256: createHash('sha256').update(raw).digest('hex'),
29
+ };
30
+ }
31
+
32
+ export async function previewProjectRelease(options) {
33
+ const {
34
+ consumerRoot, profile, requestedVersion,
35
+ repositoryFacts = { dirtyPaths: [], existingTags: [] },
36
+ } = options;
37
+ const files = await Promise.all(
38
+ profile.versionFiles.map((path) => readVersionFile(consumerRoot, path)),
39
+ );
40
+ const currentVersion = files[0].version;
41
+ const paths = files.map(({ path }) => path);
42
+ const invalidFiles = files.filter(({ version }) => {
43
+ try { parseSemver(version); return false; } catch { return true; }
44
+ });
45
+ if (invalidFiles.length) {
46
+ return {
47
+ status: 'blocked',
48
+ summary: {
49
+ currentVersion,
50
+ targetVersion: null,
51
+ packageCount: paths.length,
52
+ synchronizedFiles: paths,
53
+ },
54
+ blockers: [{
55
+ code: 'invalid-current-version',
56
+ files: invalidFiles.map(({ path, version }) => ({ path, version })),
57
+ }],
58
+ actions: [],
59
+ repositoryFacts,
60
+ };
61
+ }
62
+ let targetVersion;
63
+ try {
64
+ targetVersion = nextVersion(currentVersion, requestedVersion);
65
+ } catch {
66
+ return {
67
+ status: 'blocked',
68
+ summary: {
69
+ currentVersion,
70
+ targetVersion: null,
71
+ packageCount: paths.length,
72
+ synchronizedFiles: paths,
73
+ },
74
+ blockers: [{ code: 'invalid-target-version', requestedVersion }],
75
+ actions: [],
76
+ repositoryFacts,
77
+ };
78
+ }
79
+ const tag = `${profile.tagPrefix ?? 'v'}${targetVersion}`;
80
+ const blockers = [];
81
+ if (files.some(({ version }) => version !== currentVersion)) {
82
+ blockers.push({
83
+ code: 'divergent-versions',
84
+ files: files.map(({ path, version }) => ({ path, version })),
85
+ });
86
+ }
87
+ const dirtyTargets = paths.filter((path) => repositoryFacts.dirtyPaths.includes(path));
88
+ if (dirtyTargets.length) blockers.push({ code: 'dirty-targets', paths: dirtyTargets });
89
+ if (repositoryFacts.existingTags.includes(tag)) {
90
+ blockers.push({ code: 'existing-tag', tag });
91
+ }
92
+ const snapshot = files.map(({ path, sha256 }) => ({ path, sha256 }));
93
+ const confirmation = createHash('sha256').update(JSON.stringify({
94
+ currentVersion, targetVersion, snapshot, tag,
95
+ })).digest('hex');
96
+ return {
97
+ status: blockers.length ? 'blocked' : 'ready',
98
+ summary: {
99
+ currentVersion,
100
+ targetVersion,
101
+ packageCount: paths.length,
102
+ synchronizedFiles: paths,
103
+ },
104
+ blockers,
105
+ actions: [
106
+ { type: 'write-version', version: targetVersion, paths },
107
+ { type: 'commit', message: `chore: prepare project release ${targetVersion}` },
108
+ { type: 'tag', name: tag },
109
+ ],
110
+ repositoryFacts,
111
+ snapshot,
112
+ confirmation,
113
+ };
114
+ }
@@ -0,0 +1,25 @@
1
+ const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
2
+
3
+ export function parseSemver(version) {
4
+ const match = SEMVER.exec(version);
5
+ if (!match) throw new Error(`invalid semver: ${version}`);
6
+ return match.slice(1).map(Number);
7
+ }
8
+
9
+ export function compareSemver(left, right) {
10
+ const a = parseSemver(left);
11
+ const b = parseSemver(right);
12
+ for (let index = 0; index < a.length; index += 1) {
13
+ if (a[index] !== b[index]) return Math.sign(a[index] - b[index]);
14
+ }
15
+ return 0;
16
+ }
17
+
18
+ export function nextVersion(current, requested) {
19
+ const [major, minor, patch] = parseSemver(current);
20
+ if (requested === 'major') return `${major + 1}.0.0`;
21
+ if (requested === 'minor') return `${major}.${minor + 1}.0`;
22
+ if (requested === 'patch') return `${major}.${minor}.${patch + 1}`;
23
+ parseSemver(requested);
24
+ return requested;
25
+ }