agentic-workflow-manager 9.19.0 → 9.20.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 (59) hide show
  1. package/dist/src/commands/change/index.js +492 -0
  2. package/dist/src/commands/changes/index.js +84 -0
  3. package/dist/src/commands/sensors/index.js +9 -0
  4. package/dist/src/commands/sensors/run.js +1 -0
  5. package/dist/src/core/changes/command.js +38 -0
  6. package/dist/src/core/changes/derive.js +120 -0
  7. package/dist/src/core/changes/fs-safe.js +47 -0
  8. package/dist/src/core/changes/git-env.js +68 -0
  9. package/dist/src/core/changes/git.js +216 -0
  10. package/dist/src/core/changes/list-parse.js +67 -0
  11. package/dist/src/core/changes/list.js +408 -0
  12. package/dist/src/core/changes/parse.js +72 -0
  13. package/dist/src/core/changes/range.js +53 -0
  14. package/dist/src/core/changes/receipt.js +274 -0
  15. package/dist/src/core/changes/record-io.js +149 -0
  16. package/dist/src/core/changes/render-list.js +109 -0
  17. package/dist/src/core/changes/render.js +178 -0
  18. package/dist/src/core/changes/text-safety.js +116 -0
  19. package/dist/src/core/changes/trailers.js +76 -0
  20. package/dist/src/core/changes/types.js +26 -0
  21. package/dist/src/core/changes/validate-shape.js +219 -0
  22. package/dist/src/core/changes/worktree.js +53 -0
  23. package/dist/src/index.js +4 -0
  24. package/dist/src/release/orchestrator.js +3 -1
  25. package/dist/tests/commands/change/new.test.js +1126 -0
  26. package/dist/tests/commands/change/validate.test.js +1145 -0
  27. package/dist/tests/commands/changes/list.test.js +423 -0
  28. package/dist/tests/commands/sensors/index.test.js +1 -1
  29. package/dist/tests/core/changes/derive.test.js +455 -0
  30. package/dist/tests/core/changes/fs-safe.test.js +82 -0
  31. package/dist/tests/core/changes/git-env.test.js +143 -0
  32. package/dist/tests/core/changes/git.test.js +442 -0
  33. package/dist/tests/core/changes/helpers.js +269 -0
  34. package/dist/tests/core/changes/list-fixtures.js +122 -0
  35. package/dist/tests/core/changes/list-long-history.test.js +52 -0
  36. package/dist/tests/core/changes/list-paging.test.js +511 -0
  37. package/dist/tests/core/changes/list-parse.test.js +120 -0
  38. package/dist/tests/core/changes/list-tree.test.js +44 -0
  39. package/dist/tests/core/changes/list.test.js +821 -0
  40. package/dist/tests/core/changes/parse.test.js +142 -0
  41. package/dist/tests/core/changes/predicates.test.js +83 -0
  42. package/dist/tests/core/changes/range.test.js +92 -0
  43. package/dist/tests/core/changes/receipt.test.js +947 -0
  44. package/dist/tests/core/changes/record-io.test.js +307 -0
  45. package/dist/tests/core/changes/record.test.js +549 -0
  46. package/dist/tests/core/changes/render-list.test.js +212 -0
  47. package/dist/tests/core/changes/render.test.js +202 -0
  48. package/dist/tests/core/changes/text-safety.test.js +226 -0
  49. package/dist/tests/core/changes/trailers.test.js +181 -0
  50. package/dist/tests/core/changes/worktree.test.js +259 -0
  51. package/dist/tests/release/orchestrator-trailer.test.js +72 -0
  52. package/dist/tests/release/orchestrator.test.js +1 -0
  53. package/dist/tests/structural/change-commands-are-registered.test.js +29 -0
  54. package/dist/tests/structural/change-record-git-and-platform-conventions.test.js +77 -0
  55. package/package.json +1 -1
  56. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  57. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  58. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  59. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -0,0 +1,259 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const fs_1 = __importDefault(require("fs"));
40
+ const os_1 = __importDefault(require("os"));
41
+ const path_1 = __importDefault(require("path"));
42
+ const child_process_1 = __importStar(require("child_process"));
43
+ const helpers_1 = require("./helpers");
44
+ const worktree_1 = require("../../../src/core/changes/worktree");
45
+ const ENV_KEYS = ['HOME', 'AWM_HOME', 'GIT_CONFIG_GLOBAL', 'GIT_CONFIG_NOSYSTEM'];
46
+ const ORIGINAL = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
47
+ let saved = {};
48
+ let root;
49
+ let home;
50
+ // Jest's sandboxed `process.env` is a copy that children do not see, so the child's environment is passed explicitly.
51
+ const gitEnv = () => ({ ...process.env, HOME: home, GIT_CONFIG_GLOBAL: path_1.default.join(home, 'gitconfig'), GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t', GIT_CONFIG_NOSYSTEM: '1' });
52
+ const g = (...args) => (0, child_process_1.execFileSync)('git', ['-c', 'commit.gpgsign=false', ...args], { cwd: root, encoding: 'utf8', env: gitEnv() }).trim();
53
+ const real = child_process_1.default.execFileSync;
54
+ let spy;
55
+ beforeEach(() => {
56
+ saved = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
57
+ home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-home-'));
58
+ Object.assign(process.env, { HOME: home, AWM_HOME: path_1.default.join(home, '.awm'), GIT_CONFIG_GLOBAL: (0, helpers_1.emptyGitConfig)(home), GIT_CONFIG_NOSYSTEM: '1' });
59
+ root = fs_1.default.realpathSync.native(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-wt-')));
60
+ g('init', '-q', '-b', 'main');
61
+ fs_1.default.writeFileSync(path_1.default.join(root, 'a.txt'), 'a');
62
+ g('add', '.');
63
+ g('commit', '-q', '-m', 'feat: a');
64
+ });
65
+ afterEach(() => {
66
+ spy?.mockRestore();
67
+ spy = undefined;
68
+ for (const [k, v] of Object.entries(saved)) {
69
+ if (v === undefined)
70
+ delete process.env[k];
71
+ else
72
+ process.env[k] = v;
73
+ }
74
+ fs_1.default.rmSync(root, { recursive: true, force: true });
75
+ fs_1.default.rmSync(home, { recursive: true, force: true });
76
+ });
77
+ afterAll(() => {
78
+ // The environment the suite started with is back, so nothing leaks into the next test file.
79
+ for (const k of ENV_KEYS)
80
+ expect(process.env[k]).toBe(ORIGINAL[k]);
81
+ });
82
+ /** Intercepts git invocations whose first argument is `first`; every other call reaches the real git. */
83
+ const intercept = (first, handler) => {
84
+ spy = jest.spyOn(child_process_1.default, 'execFileSync').mockImplementation(((cmd, args, opts) => {
85
+ if (cmd === 'git' && args[0] === first)
86
+ return handler(opts, args);
87
+ return real(cmd, args, opts);
88
+ }));
89
+ };
90
+ const fail = (code) => () => { throw Object.assign(new Error(`spawnSync git ${code}`), { code }); };
91
+ describe('worktree state', () => {
92
+ test('R2.x isolation: HOME and AWM_HOME point at a throwaway directory, never the real ones', () => {
93
+ expect(process.env.HOME).toBe(home);
94
+ expect(process.env.HOME).not.toBe(ORIGINAL.HOME);
95
+ expect(process.env.AWM_HOME).toBe(path_1.default.join(home, '.awm'));
96
+ expect(process.env.AWM_HOME).not.toBe(ORIGINAL.AWM_HOME);
97
+ expect(process.env.GIT_CONFIG_GLOBAL).toBe(path_1.default.join(home, 'gitconfig'));
98
+ expect(fs_1.default.readFileSync(path_1.default.join(home, 'gitconfig'), 'utf8')).toBe('');
99
+ expect(process.env.GIT_CONFIG_NOSYSTEM).toBe('1');
100
+ });
101
+ test('the runtime-state exclusion list is exactly the local state paths', () => {
102
+ expect([...worktree_1.RUNTIME_STATE_EXCLUDES]).toEqual(['.awm/sensors/runs', '.awm/journal', '.awm/ledger', '.awm/evidence', '.awm/track.json']);
103
+ });
104
+ test('a clean repository reports its root, HEAD and a clean tree', () => {
105
+ const state = (0, worktree_1.gitState)(root, true);
106
+ expect(state.kind).toBe('ok');
107
+ if (state.kind === 'ok') {
108
+ expect(fs_1.default.realpathSync.native(state.root)).toBe(fs_1.default.realpathSync.native(root));
109
+ expect(state.commit).toBe(g('rev-parse', 'HEAD'));
110
+ expect(state.dirty).toBe(false);
111
+ }
112
+ });
113
+ test('the root is normalized with native separators', () => {
114
+ intercept('rev-parse', (opts, args) => (args[1] === '--show-toplevel' ? `${root}${path_1.default.sep}.${path_1.default.sep}\n` : real('git', args, opts)));
115
+ const state = (0, worktree_1.gitState)(root, true);
116
+ expect(state.kind === 'ok' && state.root).toBe(path_1.default.resolve(root));
117
+ });
118
+ test('an untracked file is dirty for the pre-run measure and clean for the post-run recheck', () => {
119
+ fs_1.default.writeFileSync(path_1.default.join(root, 'new.ts'), 'x');
120
+ const pre = (0, worktree_1.gitState)(root, true);
121
+ const post = (0, worktree_1.gitState)(root, false);
122
+ expect(pre.kind === 'ok' && pre.dirty).toBe(true);
123
+ expect(post.kind === 'ok' && post.dirty).toBe(false);
124
+ });
125
+ test('the pre-run measure sees untracked files even when status.showUntrackedFiles is no', () => {
126
+ g('config', 'status.showUntrackedFiles', 'no');
127
+ fs_1.default.writeFileSync(path_1.default.join(root, 'new.ts'), 'x');
128
+ const pre = (0, worktree_1.gitState)(root, true);
129
+ expect(pre.kind === 'ok' && pre.dirty).toBe(true);
130
+ });
131
+ test('a tracked change is dirty for both measures', () => {
132
+ fs_1.default.writeFileSync(path_1.default.join(root, 'a.txt'), 'changed');
133
+ const pre = (0, worktree_1.gitState)(root, true);
134
+ const post = (0, worktree_1.gitState)(root, false);
135
+ expect(pre.kind === 'ok' && pre.dirty).toBe(true);
136
+ expect(post.kind === 'ok' && post.dirty).toBe(true);
137
+ });
138
+ test.each(worktree_1.RUNTIME_STATE_EXCLUDES.map((p) => [p]))('an untracked file at %s is not a change', (rel) => {
139
+ fs_1.default.mkdirSync(path_1.default.dirname(path_1.default.join(root, rel)), { recursive: true });
140
+ fs_1.default.writeFileSync(path_1.default.join(root, rel), 'x');
141
+ const pre = (0, worktree_1.gitState)(root, true);
142
+ expect(pre.kind === 'ok' && pre.dirty).toBe(false);
143
+ });
144
+ test('extra exclusions hide changes under them, on top of the runtime-state ones', () => {
145
+ fs_1.default.mkdirSync(path_1.default.join(root, 'docs/changes'), { recursive: true });
146
+ fs_1.default.writeFileSync(path_1.default.join(root, 'docs/changes/r.md'), 'r');
147
+ const without = (0, worktree_1.gitState)(root, true);
148
+ const withExtra = (0, worktree_1.gitState)(root, true, ['docs/changes']);
149
+ expect(without.kind === 'ok' && without.dirty).toBe(true);
150
+ expect(withExtra.kind === 'ok' && withExtra.dirty).toBe(false);
151
+ fs_1.default.writeFileSync(path_1.default.join(root, 'other.ts'), 'x');
152
+ const stillDirty = (0, worktree_1.gitState)(root, true, ['docs/changes']);
153
+ expect(stillDirty.kind === 'ok' && stillDirty.dirty).toBe(true);
154
+ });
155
+ test('extra exclusions keep the runtime-state exclusions', () => {
156
+ fs_1.default.mkdirSync(path_1.default.join(root, '.awm/journal'), { recursive: true });
157
+ fs_1.default.writeFileSync(path_1.default.join(root, '.awm/journal/j.json'), 'x');
158
+ const state = (0, worktree_1.gitState)(root, true, ['docs/changes']);
159
+ expect(state.kind === 'ok' && state.dirty).toBe(false);
160
+ });
161
+ test.each([['docs/changes'], [[7]], [['../out']], [['a\nb']], [['/abs']], [[' pad']], [Array.from({ length: 65 }, (_, i) => `d${i}`)]])('unsafe extra exclusions %j are a failure, never a throw', (extra) => {
162
+ expect((0, worktree_1.gitState)(root, true, extra)).toEqual({ kind: 'failed' });
163
+ });
164
+ test('GIT_*_PATHSPECS variables in the environment do not change the exclusions', () => {
165
+ fs_1.default.mkdirSync(path_1.default.join(root, 'docs/changes'), { recursive: true });
166
+ fs_1.default.writeFileSync(path_1.default.join(root, 'docs/changes/r.md'), 'r');
167
+ fs_1.default.mkdirSync(path_1.default.join(root, '.awm/journal'), { recursive: true });
168
+ fs_1.default.writeFileSync(path_1.default.join(root, '.awm/journal/j.json'), 'x');
169
+ const saved = process.env.GIT_LITERAL_PATHSPECS;
170
+ process.env.GIT_LITERAL_PATHSPECS = '1';
171
+ try {
172
+ const state = (0, worktree_1.gitState)(root, true, ['docs/changes']);
173
+ expect(state.kind === 'ok' && state.dirty).toBe(false);
174
+ }
175
+ finally {
176
+ if (saved === undefined)
177
+ delete process.env.GIT_LITERAL_PATHSPECS;
178
+ else
179
+ process.env.GIT_LITERAL_PATHSPECS = saved;
180
+ }
181
+ });
182
+ describe('a submodule change is a change whatever the configuration says', () => {
183
+ // Offline: vendor/sm is a nested repository registered as a gitlink, hidden by .gitmodules `ignore = all` and diff.ignoreSubmodules=all.
184
+ let sm = '';
185
+ let commitInSm = () => '';
186
+ const registerHiddenSubmodule = () => {
187
+ const hidden = (0, helpers_1.makeHiddenSubmodule)(root, (cwd, ...args) => (0, child_process_1.execFileSync)('git', ['-c', 'commit.gpgsign=false', ...args], { cwd, encoding: 'utf8', env: gitEnv() }).trim());
188
+ sm = hidden.dir;
189
+ commitInSm = hidden.commitInSm;
190
+ };
191
+ test('a clean tree holding a submodule is clean', () => {
192
+ registerHiddenSubmodule();
193
+ const state = (0, worktree_1.gitState)(root, true);
194
+ expect(state.kind === 'ok' && state.dirty).toBe(false);
195
+ });
196
+ test('an untracked file inside a clean submodule: the post-run recheck ignores it, the pre-run measure counts it', () => {
197
+ registerHiddenSubmodule();
198
+ fs_1.default.writeFileSync(path_1.default.join(sm, 'coverage.out'), 'x'); // what a sensor leaves behind
199
+ const post = (0, worktree_1.gitState)(root, false);
200
+ const pre = (0, worktree_1.gitState)(root, true);
201
+ expect(post.kind === 'ok' && post.dirty).toBe(false);
202
+ // git reports the submodule itself as modified (untracked content) in the normal-untracked mode, so the pre-run measure is dirty.
203
+ expect(pre.kind === 'ok' && pre.dirty).toBe(true);
204
+ });
205
+ test.each([[true], [false]])('modified tracked content inside a submodule makes the tree dirty (untracked=%p)', (untracked) => {
206
+ registerHiddenSubmodule();
207
+ fs_1.default.writeFileSync(path_1.default.join(sm, 'one'), 'changed');
208
+ const state = (0, worktree_1.gitState)(root, untracked);
209
+ expect(state.kind === 'ok' && state.dirty).toBe(true);
210
+ });
211
+ test.each([[true], [false]])('a submodule checked out at another commit than the gitlink makes the tree dirty (untracked=%p)', (untracked) => {
212
+ registerHiddenSubmodule();
213
+ commitInSm('two');
214
+ const state = (0, worktree_1.gitState)(root, untracked);
215
+ expect(state.kind === 'ok' && state.dirty).toBe(true);
216
+ });
217
+ });
218
+ test('a non-string or empty cwd is a failure, never a throw', () => {
219
+ expect((0, worktree_1.gitState)(undefined, true)).toEqual({ kind: 'failed' });
220
+ expect((0, worktree_1.gitState)('', true)).toEqual({ kind: 'failed' });
221
+ });
222
+ test('a directory outside git is not-repo', () => {
223
+ const plain = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-plain-'));
224
+ try {
225
+ expect((0, worktree_1.gitState)(plain, true)).toEqual({ kind: 'not-repo' });
226
+ }
227
+ finally {
228
+ fs_1.default.rmSync(plain, { recursive: true, force: true });
229
+ }
230
+ });
231
+ test('a repository without a commit is not-repo', () => {
232
+ const empty = fs_1.default.realpathSync.native(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-empty-')));
233
+ try {
234
+ (0, child_process_1.execFileSync)('git', ['init', '-q', '-b', 'main'], { cwd: empty, env: gitEnv() });
235
+ expect((0, worktree_1.gitState)(empty, true)).toEqual({ kind: 'not-repo' });
236
+ }
237
+ finally {
238
+ fs_1.default.rmSync(empty, { recursive: true, force: true });
239
+ }
240
+ });
241
+ test.each([['ENOBUFS'], ['ETIMEDOUT']])('a %s from git status is a failure, not a missing repository', (code) => {
242
+ intercept('status', fail(code));
243
+ expect((0, worktree_1.gitState)(root, true)).toEqual({ kind: 'failed' });
244
+ });
245
+ test('git missing from PATH is a failure', () => {
246
+ intercept('rev-parse', fail('ENOENT'));
247
+ expect((0, worktree_1.gitState)(root, true)).toEqual({ kind: 'failed' });
248
+ });
249
+ test('git status gets a 64 MiB buffer', () => {
250
+ let seen;
251
+ intercept('status', (opts) => { seen = opts.maxBuffer; throw Object.assign(new Error('stop'), { code: 'ENOBUFS' }); });
252
+ (0, worktree_1.gitState)(root, true);
253
+ expect(seen).toBeGreaterThanOrEqual(64 * 1024 * 1024);
254
+ });
255
+ test('the reasons are the stable user-facing strings', () => {
256
+ expect(worktree_1.REASON_NOT_REPO).toBe('not inside a git repository with a commit');
257
+ expect(worktree_1.REASON_GIT_FAILED).toBe('git command failed or timed out');
258
+ });
259
+ });
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ // cli/tests/release/orchestrator-trailer.test.ts
7
+ //
8
+ // R6.1 (#217): the bump commit must be read as a Change Record opt-out by the code that reads it
9
+ // (`hasOptOut`), not just contain the right text in a mocked argv. The orchestrator runs against a
10
+ // real tmp git repo: `git add` and `git commit` execute for real with the argv the orchestrator
11
+ // passes (never retyped here); every other command is faked.
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const path_1 = __importDefault(require("path"));
14
+ const child_process_1 = require("child_process");
15
+ const orchestrator_1 = require("../../src/release/orchestrator");
16
+ const core_1 = require("../../src/release/core");
17
+ const trailers_1 = require("../../src/core/changes/trailers");
18
+ const helpers_1 = require("../core/changes/helpers");
19
+ describe('release — the bump commit opts out of the Change Record (R6.1)', () => {
20
+ let repo;
21
+ beforeEach(() => { repo = (0, helpers_1.makeRepo)(); });
22
+ afterEach(() => repo.cleanup());
23
+ it('the commit the orchestrator creates is read as an opt-out and keeps its subject byte-identical', () => {
24
+ // Explicit environment for the child git: identity and config isolation never come from process.env alone.
25
+ const gitEnv = {
26
+ ...process.env, HOME: repo.home, GIT_CONFIG_GLOBAL: path_1.default.join(repo.home, 'gitconfig'), GIT_CONFIG_NOSYSTEM: '1',
27
+ GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t',
28
+ };
29
+ repo.commit({
30
+ 'cli/package.json': '{"version":"2.1.1"}\n',
31
+ 'cli/package-lock.json': '{"version":"2.1.1"}\n',
32
+ 'CHANGELOG.md': '# Changelog\n',
33
+ }, 'chore: seed release files');
34
+ repo.git('config', 'commit.gpgsign', 'false');
35
+ expect((0, trailers_1.hasOptOut)(repo.root, ['-1', 'HEAD'])).toBe(false);
36
+ let pkgVersion = '2.1.1';
37
+ let changelog = '# Changelog\n';
38
+ const write = (rel, content) => fs_1.default.writeFileSync(path_1.default.join(repo.root, rel), content);
39
+ const commitArgv = [];
40
+ const io = {
41
+ run(cmd, args) {
42
+ if (cmd === 'git' && (args[0] === 'add' || args[0] === 'commit')) {
43
+ if (args[0] === 'commit')
44
+ commitArgv.push(args);
45
+ return (0, child_process_1.execFileSync)('git', args, { cwd: repo.root, encoding: 'utf8', env: gitEnv });
46
+ }
47
+ if (cmd === 'git' && args[0] === 'rev-parse')
48
+ return 'main';
49
+ if (cmd === 'git' && args[0] === 'log')
50
+ return `feat: nueva${core_1.US}${core_1.RS}`;
51
+ return ''; // status, tag, npm view/publish: nothing there, nothing dirty
52
+ },
53
+ readPackageVersion: () => pkgVersion,
54
+ writePackageVersion: (v) => { pkgVersion = v; write('cli/package.json', `{"version":"${v}"}\n`); },
55
+ writeLockVersion: (v) => write('cli/package-lock.json', `{"version":"${v}"}\n`),
56
+ readChangelog: () => changelog,
57
+ writeChangelog: (c) => { changelog = c; write('CHANGELOG.md', c); },
58
+ writeNpmrc: () => { },
59
+ removeNpmrc: () => { },
60
+ today: () => '2026-06-25',
61
+ log: () => { },
62
+ env: { NPM_TOKEN: 'tok' },
63
+ };
64
+ const res = (0, orchestrator_1.release)({ dryRun: false, force: null, push: false, branch: 'main', cliDir: '/cli' }, io);
65
+ expect(res).toEqual({ released: true, version: '2.2.0' });
66
+ expect(commitArgv).toHaveLength(1); // the argv under test was really executed, once
67
+ expect(repo.git('log', '-1', '--format=%s')).toBe('chore(release): v2.2.0 [skip ci]');
68
+ expect((0, trailers_1.hasOptOut)(repo.root, ['-1', 'HEAD'])).toBe(true);
69
+ // The opt-out is a trailer paragraph of its own: the full message is subject, blank line, trailer.
70
+ expect(repo.git('log', '-1', '--format=%B')).toBe('chore(release): v2.2.0 [skip ci]\n\nChange-Record: none');
71
+ });
72
+ });
@@ -51,6 +51,7 @@ describe('release — happy path', () => {
51
51
  expect(calls).toContain('WRITE_PKG 2.2.0');
52
52
  const joined = calls.join('\n');
53
53
  expect(joined).toMatch(/git commit .*chore\(release\): v2.2.0/);
54
+ expect(joined).toMatch(/git commit .*chore\(release\): v2\.2\.0 \[skip ci\] -m Change-Record: none/);
54
55
  expect(joined).toMatch(/git tag -a v2.2.0/);
55
56
  expect(joined).toMatch(/npm publish/);
56
57
  expect(joined).toMatch(/git push origin main/);
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // The Change Record commands are reachable from the real program: `awm change new|validate` and `awm changes list`. Every command
4
+ // module is tested by registering it on a fresh Commander program, which would stay green if src/index.ts forgot the register call.
5
+ const index_1 = require("../../src/index");
6
+ const find = (parent, name) => parent.commands.find((c) => c.name() === name);
7
+ const flags = (command) => (command?.options ?? []).map((o) => o.long ?? o.flags).sort();
8
+ describe('the real awm program registers the Change Record commands', () => {
9
+ test('`awm change` has `new <slug>` and `validate [path]` with their options', () => {
10
+ const change = find(index_1.program, 'change');
11
+ expect(change).toBeDefined();
12
+ const created = find(change, 'new');
13
+ const validated = find(change, 'validate');
14
+ expect(created?.usage()).toContain('<slug>');
15
+ expect(flags(created)).toEqual(['--base', '--issue', '--model', '--plan', '--refresh', '--session']);
16
+ expect(validated?.usage()).toContain('[path]');
17
+ expect(flags(validated)).toEqual(['--base', '--json', '--strict']);
18
+ });
19
+ test('`awm changes` has `list` with --since, --base and --format', () => {
20
+ const changes = find(index_1.program, 'changes');
21
+ expect(changes).toBeDefined();
22
+ expect(flags(find(changes, 'list'))).toEqual(['--base', '--format', '--since']);
23
+ });
24
+ test('both are in the top-level help', () => {
25
+ const help = index_1.program.helpInformation();
26
+ expect(help).toMatch(/^\s+change\b/m);
27
+ expect(help).toMatch(/^\s+changes\b/m);
28
+ });
29
+ });
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ // Structural guards for two defect classes that recurred through the Change Record cycle (#217,
3
+ // harness-retro 2026-10-17; see the branch ledger signatures `s3-cq-dirty-duplicates-gitstate`,
4
+ // `git-dir-env-not-scrubbed` and `silent-return-instead-of-skip`):
5
+ //
6
+ // 1. Every git child of the change-record code runs through `execGit`/`runGit`/`gitState`
7
+ // (core/changes/git-env.ts), which scrubs inherited GIT_* location/pathspec variables, closes
8
+ // stdin, bounds time and output and sets GIT_NO_LAZY_FETCH. A bare `execFileSync('git', ...)`
9
+ // in these directories silently loses all of that, and reviewers found such leaks four times.
10
+ // 2. A test that returns early on Windows is reported as PASSED by the 3-platform release gate.
11
+ // These suites use `posixOnly` (test.skip with a visible reason) instead.
12
+ //
13
+ // The detectors are exercised on inline samples below, so each guard demonstrably fires.
14
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.bareGitSpawns = bareGitSpawns;
19
+ exports.silentWin32Returns = silentWin32Returns;
20
+ const fs_1 = __importDefault(require("fs"));
21
+ const path_1 = __importDefault(require("path"));
22
+ const CLI_ROOT = path_1.default.join(__dirname, '../..');
23
+ function tsFiles(dir) {
24
+ if (!fs_1.default.existsSync(dir))
25
+ return [];
26
+ const out = [];
27
+ for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
28
+ const full = path_1.default.join(dir, entry.name);
29
+ if (entry.isDirectory())
30
+ out.push(...tsFiles(full));
31
+ else if (entry.isFile() && entry.name.endsWith('.ts'))
32
+ out.push(full);
33
+ }
34
+ return out;
35
+ }
36
+ // Naive comment stripping, as in exec-invocation-explicit-stdio.test.ts: a false positive fails loud.
37
+ function stripComments(source) {
38
+ return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1');
39
+ }
40
+ const BARE_GIT_SPAWN = /\b(?:execFileSync|execFile|spawnSync|spawn)\(\s*['"`]git['"`]/;
41
+ const SILENT_WIN32_RETURN = /\bif\s*\(\s*(?:process\.platform\s*===\s*['"]win32['"]|win32|isWindows)\s*\)\s*(?:\{\s*)?return\b/;
42
+ function bareGitSpawns(source) {
43
+ return BARE_GIT_SPAWN.test(stripComments(source));
44
+ }
45
+ function silentWin32Returns(source) {
46
+ return SILENT_WIN32_RETURN.test(stripComments(source));
47
+ }
48
+ describe('change-record conventions (structural)', () => {
49
+ test('the detectors fire on the offending shapes and not on the sanctioned ones', () => {
50
+ expect(bareGitSpawns("execFileSync('git', ['status'])")).toBe(true);
51
+ expect(bareGitSpawns('spawnSync("git", args, opts)')).toBe(true);
52
+ expect(bareGitSpawns("runGit(root, ['status'])")).toBe(false);
53
+ expect(bareGitSpawns("// execFileSync('git', [])\nexecGit(root, [])")).toBe(false);
54
+ expect(silentWin32Returns("if (process.platform === 'win32') return;")).toBe(true);
55
+ expect(silentWin32Returns('if (win32) { return; }')).toBe(true);
56
+ expect(silentWin32Returns('posixOnly(() => { /* body */ });')).toBe(false);
57
+ expect(silentWin32Returns("// if (process.platform === 'win32') return;")).toBe(false);
58
+ });
59
+ test('git is spawned only by git-env.ts under core/changes and the change commands', () => {
60
+ const dirs = ['src/core/changes', 'src/commands/change', 'src/commands/changes'].map(d => path_1.default.join(CLI_ROOT, d));
61
+ const allowed = path_1.default.join(CLI_ROOT, 'src/core/changes/git-env.ts');
62
+ const offenders = dirs
63
+ .flatMap(tsFiles)
64
+ .filter(file => file !== allowed)
65
+ .filter(file => bareGitSpawns(fs_1.default.readFileSync(file, 'utf8')))
66
+ .map(file => path_1.default.relative(CLI_ROOT, file));
67
+ expect(offenders).toEqual([]);
68
+ });
69
+ test('change-record tests skip with posixOnly instead of returning early on Windows', () => {
70
+ const dirs = ['tests/core/changes', 'tests/commands/change', 'tests/commands/changes'].map(d => path_1.default.join(CLI_ROOT, d));
71
+ const offenders = dirs
72
+ .flatMap(tsFiles)
73
+ .filter(file => silentWin32Returns(fs_1.default.readFileSync(file, 'utf8')))
74
+ .map(file => path_1.default.relative(CLI_ROOT, file));
75
+ expect(offenders).toEqual([]);
76
+ });
77
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "9.19.0",
3
+ "version": "9.20.0",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"
Binary file
Binary file