@ikon85/agent-workflow-kit 0.28.0 → 0.29.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.
Files changed (32) hide show
  1. package/.agents/skills/board-to-waves/SKILL.md +13 -3
  2. package/.agents/skills/to-issues/SKILL.md +102 -27
  3. package/.agents/skills/to-prd/SKILL.md +13 -3
  4. package/.agents/skills/to-waves/SKILL.md +16 -4
  5. package/.agents/skills/wrapup/SKILL.md +0 -1
  6. package/.claude/skills/board-to-waves/SKILL.md +13 -3
  7. package/.claude/skills/codex-build/SKILL.md +69 -23
  8. package/.claude/skills/codex-review/SKILL.md +47 -37
  9. package/.claude/skills/grill-me-codex/SKILL.md +45 -34
  10. package/.claude/skills/grill-with-docs-codex/SKILL.md +46 -34
  11. package/.claude/skills/to-issues/SKILL.md +102 -27
  12. package/.claude/skills/to-prd/SKILL.md +13 -3
  13. package/.claude/skills/to-waves/SKILL.md +16 -4
  14. package/.claude/skills/wrapup/SKILL.md +0 -1
  15. package/README.md +23 -0
  16. package/agent-workflow-kit.package.json +56 -16
  17. package/docs/research/wave-152-consumer-acceptance.md +98 -0
  18. package/package.json +1 -1
  19. package/scripts/board-sync.py +3 -7
  20. package/scripts/build-kit.test.mjs +42 -1
  21. package/scripts/codex-exec-scenarios/fake-codex.mjs +152 -0
  22. package/scripts/codex-exec.sh +566 -0
  23. package/scripts/codex-exec.test.mjs +815 -0
  24. package/scripts/codex_proc.py +602 -0
  25. package/scripts/find-by-marker.py +68 -0
  26. package/scripts/marker_lib.py +88 -0
  27. package/scripts/render-anchor.py +111 -0
  28. package/scripts/test_marker_lib.py +154 -0
  29. package/scripts/test_render_anchor.py +267 -0
  30. package/scripts/test_retro_wrapup_contract.py +6 -0
  31. package/scripts/test_skill_codex_exec_lifecycle.py +123 -0
  32. package/src/lib/bundle.mjs +10 -0
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitVersion": "0.28.0",
2
+ "kitVersion": "0.29.0",
3
3
  "files": [
4
4
  {
5
5
  "path": ".agents/skills/ask-matt/SKILL.md",
@@ -37,7 +37,7 @@
37
37
  "ownerSkill": "board-to-waves",
38
38
  "surface": "codex",
39
39
  "installRole": "consumer",
40
- "sha256": "1e145fd3da4426aa210169d9a47c5558588ee7317cbeb4ae8d0852c2cd672943",
40
+ "sha256": "aaa326544b53b43635f1f212ac3bd18a368e9440c41de748c4eccc911bc615bb",
41
41
  "mode": 420,
42
42
  "origin": "kit"
43
43
  },
@@ -817,7 +817,7 @@
817
817
  "ownerSkill": "to-issues",
818
818
  "surface": "codex",
819
819
  "installRole": "consumer",
820
- "sha256": "ed0acff1d04f04b55b9350e360c8249d1e4dbd44fd445909f11703361746e931",
820
+ "sha256": "2fc271cda4d1b46b82ac32a9e8aa04cf4349bbac0ea31133ee03458d2c6c2b13",
821
821
  "mode": 420,
822
822
  "origin": "kit"
823
823
  },
@@ -847,7 +847,7 @@
847
847
  "ownerSkill": "to-prd",
848
848
  "surface": "codex",
849
849
  "installRole": "consumer",
850
- "sha256": "0c7c556dfae2641a452ba5874e2e977b3e6f33d941b40be93b5009cb1d959ff8",
850
+ "sha256": "33c5229fcdae4309c2a5c7ea28c60b8708be276960c628f906a6da5a4450cbbb",
851
851
  "mode": 420,
852
852
  "origin": "kit"
853
853
  },
@@ -867,7 +867,7 @@
867
867
  "ownerSkill": "to-waves",
868
868
  "surface": "codex",
869
869
  "installRole": "consumer",
870
- "sha256": "5ac4fd5d26d6ea2327fc50ad4bcc8f3d8c80186afb976410d8c3e8f3c21d896e",
870
+ "sha256": "8bed0af37f81f0160079c316f26815f220e1eb3e47359a723262f6fe37978ea0",
871
871
  "mode": 420,
872
872
  "origin": "kit"
873
873
  },
@@ -957,7 +957,7 @@
957
957
  "ownerSkill": "wrapup",
958
958
  "surface": "codex",
959
959
  "installRole": "consumer",
960
- "sha256": "69cf375285a8c793e3b55e70047bf114ec12ab529a705127da0730ca42db238e",
960
+ "sha256": "0b973d9531514f4400e107858ff0070c522d5e542c47123473667ca8337627fd",
961
961
  "mode": 420,
962
962
  "origin": "kit"
963
963
  },
@@ -1199,7 +1199,7 @@
1199
1199
  "ownerSkill": "board-to-waves",
1200
1200
  "surface": "claude",
1201
1201
  "installRole": "consumer",
1202
- "sha256": "28c47d10f7508cadf5056c66a62d5891dfb61733a30ae517be24dd766c7996dd",
1202
+ "sha256": "1433f500d64adfd0365c0ec74366075fd54e053846c95aafa5ee6773a5fb2ea4",
1203
1203
  "mode": 420,
1204
1204
  "origin": "kit"
1205
1205
  },
@@ -1269,7 +1269,7 @@
1269
1269
  "ownerSkill": "codex-build",
1270
1270
  "surface": "claude",
1271
1271
  "installRole": "consumer",
1272
- "sha256": "5f546bfc08b37a829ccb16faab497a69146a7bc4118578b1fe080356f471a5a2",
1272
+ "sha256": "ddf64d487b8bbfd8cbd0cc5cf8eaff81fb46576f201fe29ad20d48f9e5055e13",
1273
1273
  "mode": 420,
1274
1274
  "origin": "kit"
1275
1275
  },
@@ -1289,7 +1289,7 @@
1289
1289
  "ownerSkill": "codex-review",
1290
1290
  "surface": "claude",
1291
1291
  "installRole": "consumer",
1292
- "sha256": "77ee99adbbd69cd21bb431e8aa0457dd8c55f5ab7c0ca4af120c91b77af68de3",
1292
+ "sha256": "053da37cb29396d613383f90b0abf3e6ce6279c22a062921dcf50348ca30fd95",
1293
1293
  "mode": 420,
1294
1294
  "origin": "kit"
1295
1295
  },
@@ -1429,7 +1429,7 @@
1429
1429
  "ownerSkill": "grill-me-codex",
1430
1430
  "surface": "claude",
1431
1431
  "installRole": "consumer",
1432
- "sha256": "ab73dd6b147e05837082db5d5ab68ab118e51e59767a717c8b7a0262cfbd07b5",
1432
+ "sha256": "77e0254e203232909898f868bb803aaff15dcc0c62515d9366b1dfda79fa249f",
1433
1433
  "mode": 420,
1434
1434
  "origin": "kit"
1435
1435
  },
@@ -1489,7 +1489,7 @@
1489
1489
  "ownerSkill": "grill-with-docs-codex",
1490
1490
  "surface": "claude",
1491
1491
  "installRole": "consumer",
1492
- "sha256": "5c7635bf31a3dc575762793ae80dfe7a26498aedcf7f1ce0513822ea1ebdb100",
1492
+ "sha256": "095d6aa09d88d07178677fb02a7e5c830d22fc60fe517bddfd283856c3dd2264",
1493
1493
  "mode": 420,
1494
1494
  "origin": "kit"
1495
1495
  },
@@ -2119,7 +2119,7 @@
2119
2119
  "ownerSkill": "to-issues",
2120
2120
  "surface": "claude",
2121
2121
  "installRole": "consumer",
2122
- "sha256": "29795b85ec699433ec88cfd1b48c8996160eda307974056f19d66226d9541f09",
2122
+ "sha256": "c53523f2b7b4023ea33cb25903d7b7a018041ac135fa11777f64fa4bebfcba11",
2123
2123
  "mode": 420,
2124
2124
  "origin": "kit"
2125
2125
  },
@@ -2149,7 +2149,7 @@
2149
2149
  "ownerSkill": "to-prd",
2150
2150
  "surface": "claude",
2151
2151
  "installRole": "consumer",
2152
- "sha256": "38352d6e792114cc6c2352c34af841033ccaf06cf34e9ccab1bc76b51a0ca4a1",
2152
+ "sha256": "9fd7582508fb33cbca2ce945a628b43b502fe942a83d3a16ff495f7c42e59109",
2153
2153
  "mode": 420,
2154
2154
  "origin": "kit"
2155
2155
  },
@@ -2169,7 +2169,7 @@
2169
2169
  "ownerSkill": "to-waves",
2170
2170
  "surface": "claude",
2171
2171
  "installRole": "consumer",
2172
- "sha256": "5ac4fd5d26d6ea2327fc50ad4bcc8f3d8c80186afb976410d8c3e8f3c21d896e",
2172
+ "sha256": "8bed0af37f81f0160079c316f26815f220e1eb3e47359a723262f6fe37978ea0",
2173
2173
  "mode": 420,
2174
2174
  "origin": "kit"
2175
2175
  },
@@ -2259,7 +2259,7 @@
2259
2259
  "ownerSkill": "wrapup",
2260
2260
  "surface": "claude",
2261
2261
  "installRole": "consumer",
2262
- "sha256": "eece58a858c826f8690a1443bc125e7550e6224ffccf909c06b9796d51e245e7",
2262
+ "sha256": "7b3266caaf53d0eadae2e16617fbf89fa170c50aaa9a916023c37afb41f05a79",
2263
2263
  "mode": 420,
2264
2264
  "origin": "kit"
2265
2265
  },
@@ -2363,7 +2363,7 @@
2363
2363
  "path": "scripts/board-sync.py",
2364
2364
  "kind": "script",
2365
2365
  "installRole": "consumer",
2366
- "sha256": "64444f57456b9032f427650fe110a1f0a03924cfe0ec7293b03bdf0ab9eefa67",
2366
+ "sha256": "d0c4cd8f05c619e5d24f26796eb759467f3ae97b154f12e9526a3017fd5b1eb9",
2367
2367
  "mode": 493,
2368
2368
  "origin": "kit"
2369
2369
  },
@@ -2415,6 +2415,22 @@
2415
2415
  "mode": 420,
2416
2416
  "origin": "kit"
2417
2417
  },
2418
+ {
2419
+ "path": "scripts/codex_proc.py",
2420
+ "kind": "script",
2421
+ "installRole": "consumer",
2422
+ "sha256": "39b00b516b69dd80c34525241991b7a57d37f701d833e92a98d373be86191e05",
2423
+ "mode": 420,
2424
+ "origin": "kit"
2425
+ },
2426
+ {
2427
+ "path": "scripts/codex-exec.sh",
2428
+ "kind": "script",
2429
+ "installRole": "consumer",
2430
+ "sha256": "c5d60648cb09343996efc98614dbd0c78b18672891bf466e5a8c5aba359d5df8",
2431
+ "mode": 493,
2432
+ "origin": "kit"
2433
+ },
2418
2434
  {
2419
2435
  "path": "scripts/execute-ready-check.py",
2420
2436
  "kind": "script",
@@ -2423,6 +2439,14 @@
2423
2439
  "mode": 493,
2424
2440
  "origin": "kit"
2425
2441
  },
2442
+ {
2443
+ "path": "scripts/find-by-marker.py",
2444
+ "kind": "script",
2445
+ "installRole": "consumer",
2446
+ "sha256": "afaec50d8214dc3d89d28fbcb20faa9f997fc58158593618fcc80fb800b367ea",
2447
+ "mode": 493,
2448
+ "origin": "kit"
2449
+ },
2426
2450
  {
2427
2451
  "path": "scripts/issue_deps.py",
2428
2452
  "kind": "script",
@@ -2463,6 +2487,14 @@
2463
2487
  "mode": 493,
2464
2488
  "origin": "kit"
2465
2489
  },
2490
+ {
2491
+ "path": "scripts/marker_lib.py",
2492
+ "kind": "script",
2493
+ "installRole": "consumer",
2494
+ "sha256": "015a22041ee7a66609123c1944e915a869f208789914811674c0c350e07a7b11",
2495
+ "mode": 420,
2496
+ "origin": "kit"
2497
+ },
2466
2498
  {
2467
2499
  "path": "scripts/memory-lifecycle/index.mjs",
2468
2500
  "kind": "script",
@@ -2567,6 +2599,14 @@
2567
2599
  "mode": 420,
2568
2600
  "origin": "kit"
2569
2601
  },
2602
+ {
2603
+ "path": "scripts/render-anchor.py",
2604
+ "kind": "script",
2605
+ "installRole": "consumer",
2606
+ "sha256": "c16ca661662c35761ec1d39f6ae946319a198a0ea39892fcce484288d319fb16",
2607
+ "mode": 493,
2608
+ "origin": "kit"
2609
+ },
2570
2610
  {
2571
2611
  "path": "scripts/safety-guardrails/core.py",
2572
2612
  "kind": "script",
@@ -0,0 +1,98 @@
1
+ # Wave 152 consumer acceptance and release checklist
2
+
3
+ Verified on 2026-07-19 for candidate `52ea49856a03f6e4bb13f24913990769126f81a8`.
4
+
5
+ ## Candidate
6
+
7
+ - Source repository: `iKon85/agent-workflow-kit`
8
+ - Candidate tarball: `/tmp/wave152-final-candidate.8CRnlP/ikon85-agent-workflow-kit-0.28.0.tgz`
9
+ - Extracted kit: `/tmp/wave152-final-package.QQTOvE/package/dist-kit`
10
+ - Disposable consumer checkout: `/tmp/wave152-testreporter.ZZg8zY/checkout`
11
+ - Consumer base: `iKon85/Testreporter` `origin/main` at
12
+ `76bcaa91`
13
+
14
+ The temporary paths record the exact acceptance input. They are not release
15
+ artifacts; a repeat run creates fresh temporary directories and a fresh
16
+ test-only marker identity.
17
+
18
+ ## Repeatable procedure
19
+
20
+ 1. Build the kit from the candidate commit, create an npm tarball, extract it,
21
+ and run its `kit:build`. Install that built kit into a disposable checkout
22
+ of the consumer's current `origin/main`.
23
+ 2. Run `scripts/codex-exec.sh new` with the `review` profile and `read-only`
24
+ mode. Capture the opaque run ID, resume it for round 2, verify both rounds
25
+ return `OK`, then finalize and verify that the run state is deleted.
26
+ 3. Run `scripts/codex-exec.sh new` with the `build` profile and
27
+ `workspace-write` mode. Ask it to create only an allowlisted ignored scratch
28
+ file containing a fixed sentinel, verify the sentinel, finalize the run,
29
+ and remove the scratch file.
30
+ 4. Create a disposable consumer issue through `scripts/board-sync.py` with a
31
+ unique `wave-stub-source` marker. Resolve it through
32
+ `scripts/find-by-marker.py`, verify one exact match, move its board status to
33
+ Done, close it, and repeat the lookup. The terminal lookup must report the
34
+ closed/user-decision branch and an all-open scan must find zero disposable
35
+ issues.
36
+
37
+ ## Acceptance evidence
38
+
39
+ | Smoke | Evidence | Result |
40
+ |---|---|---|
41
+ | `codex-review` read-only | Real new round returned `OK` with a thread; resume round 2 returned `OK`; finalize deleted run state. | PASS |
42
+ | `codex-build` workspace-write | Created only ignored `_ai_temp/wave152-build-smoke.txt` with `WAVE152_BUILD_SMOKE_OK`; finalize succeeded; scratch file removed. | PASS |
43
+ | Marker roundtrip | Identity `wave-stub-source=awkit-wave152-20260719-acceptance-52ea498` created Testreporter issue #2168 through board-sync; exact lookup was unique; board status became Done; issue was closed; final lookup selected closed/user-decision; `openDisposable=0`. | PASS |
44
+
45
+ No open disposable issue or scratch file remained after acceptance. The closed
46
+ issue remains deliberate cleanup evidence and is excluded from open productive
47
+ work by the all-state lookup's closed/user-decision result.
48
+
49
+ ## Counted surface census: 31 of 31
50
+
51
+ The denominator is re-derived from the current skill manifest and the delivery
52
+ surfaces, not from the changed-file list.
53
+
54
+ | Surface class | Derivation | Covered |
55
+ |---|---|---:|
56
+ | Skill surfaces | Nine records from `.claude/skills/skill-manifest.json`: four Claude-only Codex skills (4), three dual-surface marker callers (6), dual-surface `to-issues` (2), and dual-surface `wrapup` (2). The four Codex skills intentionally have no invented `.agents` mirrors. | 14/14 |
57
+ | Bundle registrations | The five Wave helpers in `HELPER_FILES`, with their declared kind and mode. | 5/5 |
58
+ | Checked manifest entries | The same five helpers in the freshly built and checked `agent-workflow-kit.package.json`. | 5/5 |
59
+ | Test surfaces | Harness scenarios, thin-skill lifecycle, marker library/CLI, anchor renderer/promotion, wrapup contradiction absence, and bundle/manifest/tarball modes. | 6/6 |
60
+ | Shared board callsite | `scripts/board-sync.py` imports and uses the shared marker parser instead of retaining a second grammar. | 1/1 |
61
+ | **Total** | **No unexplained gap.** | **31/31** |
62
+
63
+ The exact manifest-derived skill set is:
64
+
65
+ - Claude-only: `codex-review`, `codex-build`, `grill-me-codex`,
66
+ `grill-with-docs-codex`.
67
+ - Dual-surface: `to-prd`, `to-waves`, `board-to-waves`, `to-issues`, `wrapup`.
68
+
69
+ The five helper modes are:
70
+
71
+ - `scripts/marker_lib.py` — `0644`
72
+ - `scripts/find-by-marker.py` — `0755`
73
+ - `scripts/codex-exec.sh` — `0755`
74
+ - `scripts/codex_proc.py` — `0644`
75
+ - `scripts/render-anchor.py` — `0755`
76
+
77
+ ## Release checklist
78
+
79
+ - [x] K1-K5 integrated into one candidate.
80
+ - [x] K1, K2, and K4 passed independent Standards and Spec reviews.
81
+ - [x] Candidate tarball passed all three real consumer smokes.
82
+ - [x] Disposable consumer issue and scratch file cleaned up; no open disposable
83
+ issue remains.
84
+ - [x] Bundle, built-manifest, and npm-tarball mode assertions cover all five
85
+ helpers.
86
+ - [x] Surface census is 31 of 31 with zero unexplained gaps.
87
+ - [x] Prepared the user-authorized single minor bump from 0.28.0 to 0.29.0 and
88
+ refresh checked release state.
89
+ - [x] `release:prepare -- --version 0.29.0` completed its release guard, full
90
+ test suite, manifest rebuild, and dry-run npm pack.
91
+ - [x] Re-ran the full suite (354 Node tests and 277 Python tests),
92
+ `kit:staleness` (`OK`), and `release:guard` (`OK (minor)`).
93
+ - [x] Created and extracted the final `0.29.0` npm tarball (380 package
94
+ entries); all five helper modes matched both pack metadata and extracted
95
+ filesystem modes.
96
+ - [ ] After merge, let the trusted release workflow publish and verify
97
+ `npm run release:status` reports `released` before declaring the release
98
+ complete.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikon85/agent-workflow-kit",
3
- "version": "0.28.0",
3
+ "version": "0.29.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": {
@@ -53,6 +53,7 @@ from node_kind import classify_node, PROGRAM # noqa: E402
53
53
  # "open blocker" predicate, shared with the execute-ready drift check;
54
54
  # native dependencies (API) are the truth.
55
55
  from issue_deps import open_blocker_numbers, splice_blocked_by_section # noqa: E402
56
+ from marker_lib import first_marker # noqa: E402
56
57
 
57
58
  # --- Board profile (SSOT: docs/agents/board-sync.md `board-sync:profile`) ----
58
59
  # No inline board IDs: board_config reads them from the profile so the published
@@ -162,15 +163,10 @@ def _print_dry(args: list[str]) -> None:
162
163
 
163
164
 
164
165
  # --- pure logic (directly unit-tested) ---------------------------------------
165
- _PROGRAM_CREATE_IDENTITY_RE = re.compile(
166
- r"<!--\s*(program-(?:stub|leaf)-source):\s*([^>]+?)\s*-->"
167
- )
168
-
169
-
170
166
  def extract_program_create_identity(body: str) -> Optional[str]:
171
167
  """Stable to-waves identity carried by a stub/leaf body, if present."""
172
- match = _PROGRAM_CREATE_IDENTITY_RE.search(body)
173
- return f"{match.group(1)}: {match.group(2).strip()}" if match else None
168
+ marker = first_marker(body, ("program-stub-source", "program-leaf-source"))
169
+ return f"{marker[0]}: {marker[1]}" if marker else None
174
170
 
175
171
 
176
172
  def _find_open_issue_by_program_identity(body: str) -> Optional[dict]:
@@ -1,6 +1,6 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { access, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { access, mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join, dirname } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
@@ -8,12 +8,20 @@ import { createHash } from 'node:crypto';
8
8
  import { execFileSync } from 'node:child_process';
9
9
  import { buildKit } from './build-kit.mjs';
10
10
  import { init } from '../src/commands/init.mjs';
11
+ import { HELPER_FILES } from '../src/lib/bundle.mjs';
11
12
  import { CONSUMER_MANIFEST_NAME, readManifest } from '../src/lib/manifest.mjs';
12
13
 
13
14
  const REPO = join(dirname(fileURLToPath(import.meta.url)), '..');
14
15
  const PUBLIC_CENSUS_UNIT = JSON.parse(await readFile(
15
16
  join(REPO, 'test/fixtures/census-consumers/public-unit.json'), 'utf8',
16
17
  )).paths;
18
+ const WAVE_152_HELPERS = [
19
+ { path: 'scripts/marker_lib.py', kind: 'script', mode: 0o644 },
20
+ { path: 'scripts/find-by-marker.py', kind: 'script', mode: 0o755 },
21
+ { path: 'scripts/codex-exec.sh', kind: 'script', mode: 0o755 },
22
+ { path: 'scripts/codex_proc.py', kind: 'script', mode: 0o644 },
23
+ { path: 'scripts/render-anchor.py', kind: 'script', mode: 0o755 },
24
+ ];
17
25
  async function withBuild(fn) {
18
26
  const dist = await mkdtemp(join(tmpdir(), 'awkit-build-'));
19
27
  try { return await fn(dist, await buildKit({ repoRoot: REPO, distDir: dist })); }
@@ -89,6 +97,24 @@ test('current build contains post-tag public files and repository metadata', asy
89
97
  });
90
98
  });
91
99
 
100
+ test('Wave 152 helpers keep their bundle and built-manifest modes', async () => {
101
+ assert.deepEqual(
102
+ WAVE_152_HELPERS.map(({ path }) => HELPER_FILES.find((entry) => entry.path === path)),
103
+ WAVE_152_HELPERS,
104
+ );
105
+ await withBuild(async (dist) => {
106
+ const manifest = JSON.parse(await readFile(join(dist, 'agent-workflow-kit.package.json'), 'utf8'));
107
+ const byPath = new Map(manifest.files.map((entry) => [entry.path, entry]));
108
+ assert.deepEqual(
109
+ WAVE_152_HELPERS.map(({ path }) => {
110
+ const { kind, mode, installRole } = byPath.get(path);
111
+ return { path, kind, mode, installRole };
112
+ }),
113
+ WAVE_152_HELPERS.map((entry) => ({ ...entry, installRole: 'consumer' })),
114
+ );
115
+ });
116
+ });
117
+
92
118
  test('current build contains the complete dual-surface census consumer unit', async () => {
93
119
  await withBuild(async (dist, report) => {
94
120
  const manifest = JSON.parse(await readFile(join(dist, 'agent-workflow-kit.package.json'), 'utf8'));
@@ -129,6 +155,11 @@ test('npm pack keeps product files but excludes runtime residue', async () => {
129
155
  for (const path of PUBLIC_CENSUS_UNIT) {
130
156
  assert.ok(files.includes(path), `pack missing ${path}`);
131
157
  }
158
+ const packedByPath = new Map(JSON.parse(output)[0].files.map((file) => [file.path, file]));
159
+ assert.deepEqual(
160
+ WAVE_152_HELPERS.map(({ path, mode }) => ({ path, mode: packedByPath.get(path)?.mode })),
161
+ WAVE_152_HELPERS.map(({ path, mode }) => ({ path, mode })),
162
+ );
132
163
  assert.ok(files.every((path) => !path.startsWith('.claude/logs/')));
133
164
  assert.ok(files.every((path) => !path.includes('__pycache__') && !path.endsWith('.pyc')));
134
165
  const pkg = JSON.parse(execFileSync('node', ['-p', 'JSON.stringify(require("./package.json"))'], {
@@ -147,6 +178,16 @@ test('packed scoped artifact keeps the existing npx default-bin inference', asyn
147
178
  const packed = JSON.parse(execFileSync('npm', [
148
179
  'pack', '--pack-destination', destination, '--json',
149
180
  ], { cwd: REPO, encoding: 'utf8' }));
181
+ const packedByPath = new Map(packed[0].files.map((file) => [file.path, file]));
182
+ execFileSync('tar', ['-xzf', join(destination, packed[0].filename), '-C', destination]);
183
+ for (const { path, mode } of WAVE_152_HELPERS) {
184
+ assert.equal(packedByPath.get(path)?.mode, mode, `packed mode mismatch: ${path}`);
185
+ assert.equal(
186
+ (await stat(join(destination, 'package', path))).mode & 0o777,
187
+ mode,
188
+ `extracted mode mismatch: ${path}`,
189
+ );
190
+ }
150
191
  const output = execFileSync('npx', [
151
192
  '--yes', '--offline', `./${packed[0].filename}`,
152
193
  ], { cwd: destination, encoding: 'utf8' });
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ import { appendFileSync, writeFileSync } from 'node:fs';
3
+ import { spawn } from 'node:child_process';
4
+
5
+ const args = process.argv.slice(2);
6
+ const scenario = process.env.FAKE_CODEX_SCENARIO ?? 'ok';
7
+ const pause = Number(process.env.FAKE_CODEX_PAUSE_MS ?? 1_000);
8
+
9
+ if (args.includes('--version') || args[0] === '--version') {
10
+ console.log(`codex-cli ${process.env.FAKE_CODEX_VERSION ?? '0.144.6'}`);
11
+ process.exit(0);
12
+ }
13
+ if (args[0] === 'login' && args[1] === 'status') {
14
+ if (scenario === 'auth-fail') {
15
+ console.error('Not logged in');
16
+ process.exit(1);
17
+ }
18
+ console.log('Logged in using fake credentials');
19
+ process.exit(0);
20
+ }
21
+ if (args.includes('--help')) {
22
+ const resumeHelp = args[0] === 'exec' && args[1] === 'resume';
23
+ if (process.env.FAKE_CODEX_MISSING_CAPABILITY === '1'
24
+ || (resumeHelp && process.env.FAKE_CODEX_RESUME_MISSING_CAPABILITY === '1')) {
25
+ console.log('Usage: codex exec [PROMPT]');
26
+ } else if (resumeHelp) {
27
+ console.log('Usage: codex exec resume [--config key=value] [--json] THREAD PROMPT');
28
+ } else {
29
+ console.log('Usage: codex exec [--json] [--sandbox MODE] resume THREAD');
30
+ }
31
+ process.exit(0);
32
+ }
33
+
34
+ if (process.env.FAKE_CODEX_LAUNCH_LOG) {
35
+ appendFileSync(process.env.FAKE_CODEX_LAUNCH_LOG, `${JSON.stringify(args)}\n`);
36
+ }
37
+
38
+ if (args[0] === 'exec' && args[1] === 'resume') {
39
+ const configIndex = args.indexOf('-c');
40
+ const expected = `sandbox_mode=${process.env.FAKE_EXPECTED_SANDBOX ?? 'read-only'}`;
41
+ if (args.includes('--sandbox') || configIndex < 0 || args[configIndex + 1] !== expected) {
42
+ console.error('invalid resume sandbox contract');
43
+ process.exit(64);
44
+ }
45
+ }
46
+
47
+ const emit = (value) => console.log(JSON.stringify(value));
48
+ const thread = process.env.FAKE_CODEX_THREAD ?? 'fake-thread-1';
49
+ const started = () => emit({ type: 'thread.started', thread_id: thread });
50
+ const verdict = () => emit({
51
+ type: 'item.completed',
52
+ item: { id: 'fake-item', type: 'agent_message', text: 'fake verdict' },
53
+ });
54
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
55
+
56
+ switch (scenario) {
57
+ case 'silent':
58
+ await sleep(pause);
59
+ break;
60
+ case 'startup-byte-silence':
61
+ process.stderr.write('x');
62
+ await sleep(pause);
63
+ break;
64
+ case 'quiet-post-thread':
65
+ started();
66
+ await sleep(pause);
67
+ verdict();
68
+ break;
69
+ case 'malformed':
70
+ console.log('{definitely-not-json');
71
+ break;
72
+ case 'json-null':
73
+ emit(null);
74
+ break;
75
+ case 'json-array':
76
+ emit([]);
77
+ break;
78
+ case 'json-scalar':
79
+ emit('scalar');
80
+ break;
81
+ case 'item-null':
82
+ emit({ type: 'item.completed', item: null });
83
+ break;
84
+ case 'item-array':
85
+ emit({ type: 'item.completed', item: [] });
86
+ break;
87
+ case 'event-type-null':
88
+ emit({ type: null });
89
+ break;
90
+ case 'event-type-array':
91
+ emit({ type: [] });
92
+ break;
93
+ case 'item-type-null':
94
+ emit({ type: 'item.completed', item: { type: null } });
95
+ break;
96
+ case 'item-type-array':
97
+ emit({ type: 'item.completed', item: { type: [] } });
98
+ break;
99
+ case 'thread-non-string':
100
+ emit({ type: 'thread.started', thread_id: 42 });
101
+ break;
102
+ case 'verdict-non-string':
103
+ started();
104
+ emit({ type: 'item.completed', item: { type: 'agent_message', text: 42 } });
105
+ break;
106
+ case 'missing-thread':
107
+ verdict();
108
+ break;
109
+ case 'missing-verdict':
110
+ started();
111
+ emit({ type: 'turn.completed' });
112
+ break;
113
+ case 'exec-fail':
114
+ started();
115
+ console.error('fake execution failed token=super-secret');
116
+ process.exit(23);
117
+ break;
118
+ case 'split-secret':
119
+ started();
120
+ writeFileSync(2, `${'x'.repeat(65_530)}token=super-secret`);
121
+ process.exit(24);
122
+ break;
123
+ case 'signal':
124
+ process.kill(process.pid, 'SIGTERM');
125
+ break;
126
+ case 'group-hang': {
127
+ started();
128
+ const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)']);
129
+ if (process.env.FAKE_CODEX_CHILD_PID) {
130
+ writeFileSync(process.env.FAKE_CODEX_CHILD_PID, String(child.pid));
131
+ }
132
+ await sleep(pause);
133
+ break;
134
+ }
135
+ case 'orphan-group': {
136
+ started();
137
+ const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { stdio: 'inherit' });
138
+ if (process.env.FAKE_CODEX_CHILD_PID) {
139
+ writeFileSync(process.env.FAKE_CODEX_CHILD_PID, String(child.pid));
140
+ }
141
+ child.unref();
142
+ break;
143
+ }
144
+ case 'pipe-burst':
145
+ process.stderr.write('x'.repeat(2 * 1024 * 1024));
146
+ started();
147
+ verdict();
148
+ break;
149
+ default:
150
+ started();
151
+ verdict();
152
+ }