agentic-workflow-manager 9.21.0 → 9.22.1

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 (71) hide show
  1. package/dist/src/commands/change/index.js +16 -5
  2. package/dist/src/commands/init.js +8 -4
  3. package/dist/src/commands/preflight/checks.js +22 -0
  4. package/dist/src/commands/registry/add.js +9 -3
  5. package/dist/src/commands/sensors/compatibility/contract.js +56 -3
  6. package/dist/src/commands/sensors/compatibility/manifest.js +16 -2
  7. package/dist/src/commands/sensors/index.js +10 -1
  8. package/dist/src/commands/sensors/inputs-digest.js +315 -0
  9. package/dist/src/commands/sensors/prepare.js +10 -0
  10. package/dist/src/commands/sensors/result-cache.js +278 -0
  11. package/dist/src/commands/sensors/result.js +31 -12
  12. package/dist/src/commands/sensors/run.js +265 -8
  13. package/dist/src/commands/sensors/status.js +66 -7
  14. package/dist/src/commands/sync.js +11 -5
  15. package/dist/src/commands/update.js +23 -12
  16. package/dist/src/core/changes/derive.js +6 -3
  17. package/dist/src/core/changes/git.js +1 -2
  18. package/dist/src/core/changes/receipt.js +219 -9
  19. package/dist/src/core/changes/worktree.js +15 -3
  20. package/dist/src/core/init/steps.js +3 -8
  21. package/dist/src/core/registries.js +20 -15
  22. package/dist/src/core/registry-git-errors.js +107 -0
  23. package/dist/src/core/registry-git.js +93 -0
  24. package/dist/src/core/registry-sync-report.js +74 -0
  25. package/dist/src/core/versioning.js +3 -6
  26. package/dist/src/index.js +5 -8
  27. package/dist/tests/commands/change/new.test.js +104 -58
  28. package/dist/tests/commands/change/validate-unmeasured.test.js +246 -0
  29. package/dist/tests/commands/change/validate.test.js +164 -108
  30. package/dist/tests/commands/changes/list.test.js +41 -22
  31. package/dist/tests/commands/job/gate-reconcile.test.js +13 -2
  32. package/dist/tests/commands/preflight/sensors-inputs.test.js +110 -0
  33. package/dist/tests/commands/registry-sync-honesty.test.js +175 -0
  34. package/dist/tests/commands/sensors/compatibility/contract.test.js +60 -0
  35. package/dist/tests/commands/sensors/compatibility/manifest.test.js +94 -0
  36. package/dist/tests/commands/sensors/exec.test.js +3 -1
  37. package/dist/tests/commands/sensors/inputs-digest.test.js +373 -0
  38. package/dist/tests/commands/sensors/result-cache.test.js +352 -0
  39. package/dist/tests/commands/sensors/run-cache.test.js +434 -0
  40. package/dist/tests/commands/sensors/run-slice.test.js +171 -0
  41. package/dist/tests/commands/sensors/status-inputs.test.js +271 -0
  42. package/dist/tests/commands/update.test.js +8 -8
  43. package/dist/tests/commands/watch/supervisor-loop.test.js +21 -5
  44. package/dist/tests/core/changes/helpers.js +15 -3
  45. package/dist/tests/core/changes/list-paging.test.js +131 -110
  46. package/dist/tests/core/changes/list.test.js +133 -83
  47. package/dist/tests/core/changes/receipt.test.js +279 -8
  48. package/dist/tests/core/diagnostics/provider-checks.test.js +3 -0
  49. package/dist/tests/core/init/steps-registry-sync.test.js +6 -8
  50. package/dist/tests/core/registry-git.test.js +153 -0
  51. package/dist/tests/helpers/commit-seeded-registry.js +18 -0
  52. package/dist/tests/integration/codex-provider-isolated.test.js +7 -2
  53. package/dist/tests/integration/copilot-init-isolated.test.js +7 -4
  54. package/dist/tests/structural/active-documentation.test.js +0 -41
  55. package/dist/tests/structural/bundle-skill-reference-contract.test.js +0 -12
  56. package/dist/tests/structural/cli-reference-matches-commander.test.js +94 -0
  57. package/dist/tests/structural/native-release-artifacts.test.js +116 -107
  58. package/dist/tests/structural/sensor-documentation-contract.test.js +0 -47
  59. package/dist/tests/structural/sensor-portability-contract.test.js +0 -7
  60. package/dist/tests/structural/simple-git-v4-pin.test.js +49 -0
  61. package/package.json +3 -2
  62. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  63. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  64. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  65. package/prebuilds/win32-x64/secure_fs.node +0 -0
  66. package/dist/tests/structural/cloud-admission-docs-contract.test.js +0 -54
  67. package/dist/tests/structural/documentation-phase-is-mechanized.test.js +0 -29
  68. package/dist/tests/structural/operator-environments-docs-contract.test.js +0 -70
  69. package/dist/tests/structural/process-model-description-contract.test.js +0 -34
  70. package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +0 -104
  71. package/dist/tests/structural/r4-plan-currentness-docs.test.js +0 -37
@@ -75,15 +75,18 @@ let home;
75
75
  // Child git processes get an explicit identity and config isolation; the shared makeRepo already points HOME and AWM_HOME at a throwaway home.
76
76
  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' });
77
77
  const g = (...args) => repo.git(...args);
78
- beforeEach(() => {
79
- repo = (0, helpers_1.makeRepo)();
80
- root = repo.root;
81
- home = repo.home;
78
+ // One template repository for the whole file (it also holds HOME and AWM_HOME on a throwaway home); every test gets a copy (no git).
79
+ let template;
80
+ beforeAll(() => {
81
+ template = (0, helpers_1.makeRepo)();
82
82
  // makeRepo's own .gitignore ignores `.awm/`; these tests need a repository that does NOT (R2.2b), so the first commit is replaced.
83
- repo.commit({ '.gitignore': 'node_modules\n', 'a.txt': 'a' }, 'feat: a');
83
+ template.commit({ '.gitignore': 'node_modules\n', 'a.txt': 'a' }, 'feat: a');
84
84
  });
85
+ const useRepo = (from) => { repo = (0, helpers_1.cloneRepo)(from); root = repo.root; home = repo.home; };
86
+ beforeEach(() => useRepo(template));
85
87
  afterEach(() => repo.cleanup());
86
88
  afterAll(() => {
89
+ template.cleanup(); // restores HOME, AWM_HOME and the git config variables
87
90
  for (const k of ENV_KEYS)
88
91
  expect(process.env[k]).toBe(ORIGINAL[k]);
89
92
  expect(listRuns()).toEqual(RUNS_BEFORE);
@@ -141,7 +144,7 @@ describe('receipt', () => {
141
144
  test('R2.3 the receipt carries schema, commit, ranAt, cliVersion and overall from the run', () => {
142
145
  const head = g('rev-parse', 'HEAD');
143
146
  const r = (0, receipt_1.buildReceipt)({ commit: head, ranAt: '2026-10-17T10:00:00.000Z', cliVersion: '9.20.0', output: { overall: 'fail', sensors: [{ name: 'lint', status: 'fail', errors: [] }] } });
144
- expect(r).toMatchObject({ schema: 1, commit: head, ranAt: '2026-10-17T10:00:00.000Z', cliVersion: '9.20.0', overall: 'fail', sensors: { lint: 'fail' } });
147
+ expect(r).toMatchObject({ schema: 2, commit: head, ranAt: '2026-10-17T10:00:00.000Z', cliVersion: '9.20.0', overall: 'fail', sensors: { lint: 'fail' } });
145
148
  });
146
149
  (0, helpers_1.posixOnly)('R2.1 a receipt replacing an existing 0666 file ends with mode 0600', () => {
147
150
  const head = g('rev-parse', 'HEAD');
@@ -646,7 +649,7 @@ describe('receipt', () => {
646
649
  ['unknown sensor status', (r) => { r.sensors.lint = 'ok'; }],
647
650
  ['array sensors', (r) => { r.sensors = []; }],
648
651
  ['null sensors', (r) => { r.sensors = null; }],
649
- ['schema 2', (r) => { r.schema = 2; }],
652
+ ['schema 3', (r) => { r.schema = 3; }],
650
653
  ['non-string cliVersion', (r) => { r.cliVersion = 1; }],
651
654
  ['multi-line cliVersion', (r) => { r.cliVersion = '1.0\n2.0'; }],
652
655
  ['multi-line sensor name', (r) => { r.sensors['bad\nname'] = 'pass'; }],
@@ -776,9 +779,18 @@ describe('sensors run wiring (R2.2, R2.4)', () => {
776
779
  g('commit', '-q', '-m', `chore: manifest in ${path_1.default.basename(dir)}`);
777
780
  };
778
781
  const receiptNames = () => fs_1.default.readdirSync(receiptsDir()).sort();
782
+ // The template plus `commitManifest(root)`, built once: each test of this block starts from a copy of it.
783
+ let manifested;
784
+ afterAll(() => manifested?.cleanup());
779
785
  beforeEach(() => {
780
786
  process.exitCode = undefined;
781
- commitManifest(root);
787
+ // The shared state is its own copy, never the test's `repo`: a failed copy below must not let afterEach remove it.
788
+ if (manifested === undefined) {
789
+ commitManifest(root);
790
+ manifested = (0, helpers_1.cloneRepo)(repo);
791
+ }
792
+ repo.cleanup();
793
+ useRepo(manifested);
782
794
  run_1.findManifestDir.mockReset().mockImplementation((start) => {
783
795
  const project = (0, project_1.resolveSensorProject)(start);
784
796
  return project.state === 'configured' ? project.projectRoot : null;
@@ -944,4 +956,263 @@ describe('sensors run wiring (R2.2, R2.4)', () => {
944
956
  expect(fs_1.default.existsSync(receiptsDir())).toBe(false);
945
957
  });
946
958
  });
959
+ test('RF-4.2/4.4: all-cached clean full gate still writes a HEAD receipt (digest is authority)', async () => {
960
+ run_1.runSensors.mockResolvedValue({
961
+ overall: 'pass',
962
+ sensors: [
963
+ {
964
+ name: 'lint',
965
+ status: 'pass',
966
+ errors: [],
967
+ cached: true,
968
+ cacheProvenance: {
969
+ originCommit: 'c'.repeat(40),
970
+ originDirty: false,
971
+ inputDigest: `sha256:${'d'.repeat(64)}`,
972
+ sensorFingerprint: `sha256:${'e'.repeat(64)}`,
973
+ },
974
+ measurement: {
975
+ inputs: ['src/**/*.ts'],
976
+ packageRootRel: '',
977
+ inputDigest: `sha256:${'d'.repeat(64)}`,
978
+ sensorFingerprint: `sha256:${'e'.repeat(64)}`,
979
+ measuredPaths: ['a.txt', '.awm/sensors.json'],
980
+ },
981
+ },
982
+ {
983
+ name: 'a-typecheck',
984
+ status: 'pass',
985
+ errors: [],
986
+ cached: true,
987
+ cacheProvenance: {
988
+ inputDigest: `sha256:${'f'.repeat(64)}`,
989
+ sensorFingerprint: `sha256:${'e'.repeat(64)}`,
990
+ },
991
+ measurement: {
992
+ inputs: ['src/**/*.ts'],
993
+ packageRootRel: '',
994
+ inputDigest: `sha256:${'f'.repeat(64)}`,
995
+ sensorFingerprint: `sha256:${'e'.repeat(64)}`,
996
+ measuredPaths: ['a.txt', '.awm/sensors.json'],
997
+ },
998
+ },
999
+ ],
1000
+ manifestPath: rootManifest(),
1001
+ });
1002
+ await runCli();
1003
+ expect(written(stderr)).toBe('');
1004
+ expect(process.exitCode).toBe(0);
1005
+ const head = g('rev-parse', 'HEAD');
1006
+ const read = (0, receipt_1.readReceipt)(root, head);
1007
+ expect(read.ok).toBe(true);
1008
+ if (!read.ok)
1009
+ return;
1010
+ expect(read.receipt.schema).toBe(2);
1011
+ expect(read.receipt.sensors).toEqual({ 'a-typecheck': 'pass', lint: 'pass' });
1012
+ });
1013
+ });
1014
+ describe('RF-4.3 receipt schema 2', () => {
1015
+ test('buildReceipt writes schema 2 with measurements and alwaysMeasuredPaths including baseline', () => {
1016
+ const head = g('rev-parse', 'HEAD');
1017
+ const r = (0, receipt_1.buildReceipt)({
1018
+ commit: head,
1019
+ ranAt: '2026-10-17T10:00:00.000Z',
1020
+ cliVersion: '9.21.0',
1021
+ output: {
1022
+ overall: 'pass',
1023
+ sensors: [{
1024
+ name: 'lint',
1025
+ status: 'pass',
1026
+ errors: [],
1027
+ measurement: {
1028
+ inputs: ['src/**/*.ts'],
1029
+ packageRootRel: '',
1030
+ inputDigest: `sha256:${'1'.repeat(64)}`,
1031
+ sensorFingerprint: `sha256:${'2'.repeat(64)}`,
1032
+ measuredPaths: ['src/a.ts', '.awm/sensors.json', 'package-lock.json'],
1033
+ },
1034
+ }],
1035
+ },
1036
+ alwaysMeasuredPaths: ['.awm/sensors.json', '.awm/sensors.baseline.json', 'package-lock.json'],
1037
+ });
1038
+ expect(r.schema).toBe(2);
1039
+ expect(r.sensors).toEqual({ lint: 'pass' });
1040
+ const v2 = r;
1041
+ expect(v2.measurements).toEqual({
1042
+ lint: {
1043
+ inputs: ['src/**/*.ts'],
1044
+ packageRootRel: '',
1045
+ inputDigest: `sha256:${'1'.repeat(64)}`,
1046
+ sensorFingerprint: `sha256:${'2'.repeat(64)}`,
1047
+ measuredPaths: ['.awm/sensors.json', 'package-lock.json', 'src/a.ts'],
1048
+ },
1049
+ });
1050
+ expect(v2.alwaysMeasuredPaths).toEqual(['.awm/sensors.baseline.json', '.awm/sensors.json', 'package-lock.json']);
1051
+ expect(v2.digest).toMatch(/^sha256:[0-9a-f]{64}$/);
1052
+ expect((0, receipt_1.readReceipt)(root, (() => { (0, receipt_1.writeReceipt)(root, r); return head; })()).ok).toBe(true);
1053
+ });
1054
+ test('schema 1 receipts remain readable', () => {
1055
+ const head = g('rev-parse', 'HEAD');
1056
+ const body = {
1057
+ schema: 1,
1058
+ commit: head,
1059
+ ranAt: '2026-10-17T10:00:00.000Z',
1060
+ cliVersion: '9.20.0',
1061
+ overall: 'pass',
1062
+ sensors: { lint: 'pass' },
1063
+ };
1064
+ const receipt = { ...body, digest: (0, receipt_1.computeReceiptDigest)(body) };
1065
+ (0, receipt_1.writeReceipt)(root, receipt);
1066
+ const read = (0, receipt_1.readReceipt)(root, head);
1067
+ expect(read).toEqual({ ok: true, receipt });
1068
+ });
1069
+ test('schema 2 digest covers measurements and alwaysMeasuredPaths', () => {
1070
+ const base = {
1071
+ schema: 2,
1072
+ commit: 'a'.repeat(40),
1073
+ ranAt: '2026-10-17T10:00:00.000Z',
1074
+ cliVersion: '1.0.0',
1075
+ overall: 'pass',
1076
+ sensors: { lint: 'pass' },
1077
+ measurements: {
1078
+ lint: {
1079
+ inputs: ['src/**'],
1080
+ packageRootRel: '',
1081
+ inputDigest: `sha256:${'1'.repeat(64)}`,
1082
+ sensorFingerprint: `sha256:${'2'.repeat(64)}`,
1083
+ measuredPaths: ['src/a.ts'],
1084
+ },
1085
+ },
1086
+ alwaysMeasuredPaths: ['.awm/sensors.baseline.json'],
1087
+ };
1088
+ const d = (0, receipt_1.computeReceiptDigest)(base);
1089
+ const measuredChanged = {
1090
+ ...base,
1091
+ measurements: {
1092
+ lint: { ...base.measurements.lint, measuredPaths: ['src/b.ts'] },
1093
+ },
1094
+ };
1095
+ const alwaysChanged = {
1096
+ ...base,
1097
+ alwaysMeasuredPaths: ['.awm/sensors.json'],
1098
+ };
1099
+ expect((0, receipt_1.computeReceiptDigest)(measuredChanged)).not.toBe(d);
1100
+ expect((0, receipt_1.computeReceiptDigest)(alwaysChanged)).not.toBe(d);
1101
+ });
1102
+ test('RF-4.6 helper: relaxation only when every recorded sensor has authored inputs', () => {
1103
+ expect((0, receipt_1.allowsUnmeasuredRelaxation)({
1104
+ schema: 2,
1105
+ commit: 'a'.repeat(40),
1106
+ ranAt: '2026-10-17T10:00:00.000Z',
1107
+ cliVersion: '1',
1108
+ overall: 'pass',
1109
+ sensors: { lint: 'pass', test: 'pass' },
1110
+ measurements: {
1111
+ lint: {
1112
+ inputs: ['src/**'],
1113
+ packageRootRel: '',
1114
+ inputDigest: `sha256:${'1'.repeat(64)}`,
1115
+ sensorFingerprint: `sha256:${'2'.repeat(64)}`,
1116
+ measuredPaths: ['src/a.ts'],
1117
+ },
1118
+ },
1119
+ alwaysMeasuredPaths: [],
1120
+ digest: 'sha256:' + '0'.repeat(64),
1121
+ })).toBe(false);
1122
+ expect((0, receipt_1.allowsUnmeasuredRelaxation)({
1123
+ schema: 2,
1124
+ commit: 'a'.repeat(40),
1125
+ ranAt: '2026-10-17T10:00:00.000Z',
1126
+ cliVersion: '1',
1127
+ overall: 'pass',
1128
+ sensors: { lint: 'pass' },
1129
+ measurements: {
1130
+ lint: {
1131
+ inputs: ['src/**'],
1132
+ packageRootRel: '',
1133
+ inputDigest: `sha256:${'1'.repeat(64)}`,
1134
+ sensorFingerprint: `sha256:${'2'.repeat(64)}`,
1135
+ measuredPaths: ['src/a.ts'],
1136
+ },
1137
+ },
1138
+ alwaysMeasuredPaths: ['.awm/sensors.baseline.json'],
1139
+ digest: 'sha256:' + '0'.repeat(64),
1140
+ })).toBe(true);
1141
+ expect((0, receipt_1.allowsUnmeasuredRelaxation)({
1142
+ schema: 1,
1143
+ commit: 'a'.repeat(40),
1144
+ ranAt: '2026-10-17T10:00:00.000Z',
1145
+ cliVersion: '1',
1146
+ overall: 'pass',
1147
+ sensors: { lint: 'pass' },
1148
+ digest: 'sha256:' + '0'.repeat(64),
1149
+ })).toBe(false);
1150
+ });
1151
+ test('readReceipt rejects schema-2 measurements with unsorted measuredPaths or malformed measurement shapes', () => {
1152
+ const head = g('rev-parse', 'HEAD');
1153
+ const baseMeasurement = {
1154
+ inputs: ['src/**/*.ts'],
1155
+ packageRootRel: '',
1156
+ inputDigest: `sha256:${'1'.repeat(64)}`,
1157
+ sensorFingerprint: `sha256:${'2'.repeat(64)}`,
1158
+ measuredPaths: ['a.ts', 'b.ts'],
1159
+ };
1160
+ const writeV2 = (measurements, alwaysMeasuredPaths = ['.awm/sensors.baseline.json']) => {
1161
+ const body = {
1162
+ schema: 2,
1163
+ commit: head,
1164
+ ranAt: '2026-10-17T10:00:00.000Z',
1165
+ cliVersion: '9.21.0',
1166
+ overall: 'pass',
1167
+ sensors: { lint: 'pass' },
1168
+ measurements,
1169
+ alwaysMeasuredPaths,
1170
+ };
1171
+ const receipt = { ...body, digest: (0, receipt_1.computeReceiptDigest)(body) };
1172
+ const dir = path_1.default.join(root, '.awm', 'sensors', 'runs');
1173
+ fs_1.default.mkdirSync(dir, { recursive: true });
1174
+ fs_1.default.writeFileSync(path_1.default.join(dir, `${head}.json`), JSON.stringify(receipt) + '\n');
1175
+ };
1176
+ writeV2({ lint: { ...baseMeasurement, measuredPaths: ['b.ts', 'a.ts'] } });
1177
+ expect((0, receipt_1.readReceipt)(root, head)).toEqual({ ok: false, code: 'receipt-invalid' });
1178
+ writeV2({ lint: { ...baseMeasurement, measuredPaths: 'a.ts' } });
1179
+ expect((0, receipt_1.readReceipt)(root, head)).toEqual({ ok: false, code: 'receipt-invalid' });
1180
+ writeV2({ lint: { ...baseMeasurement, inputs: [] } });
1181
+ expect((0, receipt_1.readReceipt)(root, head)).toEqual({ ok: false, code: 'receipt-invalid' });
1182
+ writeV2({ lint: { ...baseMeasurement, packageRootRel: '../escape' } });
1183
+ expect((0, receipt_1.readReceipt)(root, head)).toEqual({ ok: false, code: 'receipt-invalid' });
1184
+ });
1185
+ test('measurement.inputs on schema-2 parse reuse RF-1.5 glob rules (reject .., !, braces, extglobs)', () => {
1186
+ const head = g('rev-parse', 'HEAD');
1187
+ const mk = (inputs) => {
1188
+ const body = {
1189
+ schema: 2,
1190
+ commit: head,
1191
+ ranAt: '2026-10-17T10:00:00.000Z',
1192
+ cliVersion: '9.21.0',
1193
+ overall: 'pass',
1194
+ sensors: { lint: 'pass' },
1195
+ measurements: {
1196
+ lint: {
1197
+ inputs,
1198
+ packageRootRel: '',
1199
+ inputDigest: `sha256:${'1'.repeat(64)}`,
1200
+ sensorFingerprint: `sha256:${'2'.repeat(64)}`,
1201
+ measuredPaths: ['src/a.ts'],
1202
+ },
1203
+ },
1204
+ alwaysMeasuredPaths: ['.awm/sensors.baseline.json'],
1205
+ };
1206
+ const receipt = { ...body, digest: (0, receipt_1.computeReceiptDigest)(body) };
1207
+ const dir = path_1.default.join(root, '.awm', 'sensors', 'runs');
1208
+ fs_1.default.mkdirSync(dir, { recursive: true });
1209
+ fs_1.default.writeFileSync(path_1.default.join(dir, `${head}.json`), JSON.stringify(receipt) + '\n');
1210
+ return (0, receipt_1.readReceipt)(root, head);
1211
+ };
1212
+ expect(mk(['src/**/*.ts'])).toMatchObject({ ok: true });
1213
+ expect(mk(['../secret/**'])).toEqual({ ok: false, code: 'receipt-invalid' });
1214
+ expect(mk(['!src/**'])).toEqual({ ok: false, code: 'receipt-invalid' });
1215
+ expect(mk(['src/{a,b}.ts'])).toEqual({ ok: false, code: 'receipt-invalid' });
1216
+ expect(mk(['src/@(a|b).ts'])).toEqual({ ok: false, code: 'receipt-invalid' });
1217
+ });
947
1218
  });
@@ -29,6 +29,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
29
29
  const fs_1 = __importDefault(require("fs"));
30
30
  const os_1 = __importDefault(require("os"));
31
31
  const path_1 = __importDefault(require("path"));
32
+ const commit_seeded_registry_1 = require("../../helpers/commit-seeded-registry");
32
33
  describe('gatherProviderChecks — realistic fully-healthy single-provider fixtures', () => {
33
34
  let tmpHome;
34
35
  let tmpWork;
@@ -86,6 +87,8 @@ describe('gatherProviderChecks — realistic fully-healthy single-provider fixtu
86
87
  name: 'dev-core', description: '', version: '1.0.0', scope: 'baseline', visibility: 'public',
87
88
  dependsOn: [], skills: ['development-process'], workflows: [], agents,
88
89
  }));
90
+ // Local commit so real `awm init` skips network sync under SG-7 (#238).
91
+ (0, commit_seeded_registry_1.commitSeededRegistry)(root);
89
92
  fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm'), { recursive: true });
90
93
  fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.awm/registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.invalid/baseline.git' }], null, 2));
91
94
  }
@@ -6,10 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  // stepCache vs. syncRegistries()'s RESULT-shaped failures.
7
7
  //
8
8
  // `syncRegistries()` never throws on a per-registry failure — it returns
9
- // `{ action: 'error', error }` for that registry and keeps going. stepCache
10
- // used to `await` it and report a blanket 'applied', so a registry that never
11
- // landed on disk degraded silently into some later step's failure with its own
12
- // cause already gone.
9
+ // `{ action: 'error', error }` for that registry. SG-7: stepCache fails closed
10
+ // on ANY sync error (usable on-disk cache is not enough for init).
13
11
  //
14
12
  // Isolated in its own file (not steps.test.ts) because these assertions reach
15
13
  // `listRegistries()`, which resolves AWM_HOME at module require time — the env
@@ -83,12 +81,12 @@ describe('stepCache — registry sync error results', () => {
83
81
  expect(r.error).toContain('baseline');
84
82
  expect(r.error).toContain('could not clone');
85
83
  });
86
- it('stays applied but records the error when content is already on disk', async () => {
84
+ it('fails when sync errors even if content is already on disk (SG-7)', async () => {
87
85
  configureRegistries(['baseline'], ['baseline']);
88
86
  const r = await run([{ name: 'baseline', action: 'error', error: 'pull timed out' }]);
89
- expect(r.action).toBe('applied');
90
- expect(r.detail).toContain('baseline');
91
- expect(r.detail).toContain('pull timed out');
87
+ expect(r.action).toBe('failed');
88
+ expect(r.error).toContain('baseline');
89
+ expect(r.error).toContain('pull timed out');
92
90
  });
93
91
  it('fails on a secondary registry that never landed, even when the base one synced', async () => {
94
92
  configureRegistries(['baseline', 'documentation'], ['baseline']);
@@ -0,0 +1,153 @@
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
+ const fs_1 = __importDefault(require("fs"));
7
+ const os_1 = __importDefault(require("os"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const registry_git_1 = require("../../src/core/registry-git");
10
+ const registry_git_errors_1 = require("../../src/core/registry-git-errors");
11
+ describe('registry-git factory (#238 S1)', () => {
12
+ it('buildRegistryGitEnv drops guarded GIT_* keys, keeps PATH, forces GIT_TERMINAL_PROMPT=0', () => {
13
+ const parent = {
14
+ PATH: '/usr/bin',
15
+ HOME: '/tmp/home',
16
+ GIT_ASKPASS: '/secret/askpass',
17
+ GIT_SSL_CAINFO: '/secret/ca.pem',
18
+ GIT_TERMINAL_PROMPT: '1',
19
+ EDITOR: 'vi',
20
+ VISUAL: 'vim',
21
+ PAGER: 'less',
22
+ OTHER: 'ok',
23
+ };
24
+ const env = (0, registry_git_1.buildRegistryGitEnv)(parent);
25
+ expect(env.PATH).toBe('/usr/bin');
26
+ expect(env.HOME).toBe('/tmp/home');
27
+ expect(env.OTHER).toBe('ok');
28
+ expect(env.GIT_ASKPASS).toBeUndefined();
29
+ expect(env.GIT_SSL_CAINFO).toBeUndefined();
30
+ expect(env.EDITOR).toBeUndefined();
31
+ expect(env.VISUAL).toBeUndefined();
32
+ expect(env.PAGER).toBeUndefined();
33
+ expect(env.GIT_TERMINAL_PROMPT).toBe('0');
34
+ expect((0, registry_git_1.listIgnoredGuardedGitEnvNames)(parent)).toEqual(['GIT_ASKPASS', 'GIT_SSL_CAINFO']);
35
+ });
36
+ it('createRegistryGit against a bad remote fails quickly without hanging', async () => {
37
+ const dest = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-registry-git-'));
38
+ const remote = 'https://127.0.0.1:1/awm-no-such-repo.git';
39
+ const started = Date.now();
40
+ await expect((0, registry_git_1.createRegistryGit)().clone(remote, path_1.default.join(dest, 'repo'))).rejects.toThrow(/terminal prompts disabled|Failed to connect|Could not resolve|timed out|Authentication|Connection refused|Unable to connect|Network is unreachable/i);
41
+ expect(Date.now() - started).toBeLessThan(15_000);
42
+ fs_1.default.rmSync(dest, { recursive: true, force: true });
43
+ }, 20_000);
44
+ });
45
+ describe('registry-git ignored-env notice (#238 S2 / SG-5)', () => {
46
+ afterEach(() => {
47
+ (0, registry_git_1.resetIgnoredGitEnvNoticeForTests)();
48
+ });
49
+ it('listIgnoredGuardedGitEnvNames returns GIT_SSL_CAINFO only (never the value)', () => {
50
+ const parent = {
51
+ PATH: '/usr/bin',
52
+ GIT_SSL_CAINFO: '/secret/ca.pem',
53
+ GIT_TERMINAL_PROMPT: '0',
54
+ };
55
+ expect((0, registry_git_1.listIgnoredGuardedGitEnvNames)(parent)).toEqual(['GIT_SSL_CAINFO']);
56
+ const notice = (0, registry_git_1.formatIgnoredGitEnvNotice)(['GIT_SSL_CAINFO']);
57
+ expect(notice).toContain('GIT_SSL_CAINFO');
58
+ expect(notice).not.toContain('/secret/ca.pem');
59
+ expect(notice).toMatch(/git config|http\.sslCAInfo|file-based|credential\.helper|core\.sshCommand/i);
60
+ });
61
+ it('warnIgnoredGitEnvOnce emits once per process and never prints values', () => {
62
+ const lines = [];
63
+ const parent = {
64
+ GIT_ASKPASS: '/secret/askpass',
65
+ GIT_SSL_CAINFO: '/secret/ca.pem',
66
+ GIT_TERMINAL_PROMPT: '1',
67
+ };
68
+ (0, registry_git_1.warnIgnoredGitEnvOnce)((m) => lines.push(m), parent);
69
+ (0, registry_git_1.warnIgnoredGitEnvOnce)((m) => lines.push(m), parent);
70
+ expect(lines).toHaveLength(1);
71
+ expect(lines[0]).toContain('GIT_ASKPASS');
72
+ expect(lines[0]).toContain('GIT_SSL_CAINFO');
73
+ expect(lines[0]).not.toContain('/secret/');
74
+ expect(lines[0]).not.toMatch(/GIT_TERMINAL_PROMPT/);
75
+ });
76
+ it('SG-5 lists only transport/auth names (not GIT_EDITOR) and includes SSH_ASKPASS (M2/B2)', () => {
77
+ const parent = {
78
+ GIT_EDITOR: 'vim',
79
+ GIT_PAGER: 'less',
80
+ GIT_ASKPASS: '/secret/askpass',
81
+ SSH_ASKPASS: '/secret/ssh-askpass',
82
+ GIT_TERMINAL_PROMPT: '0',
83
+ };
84
+ expect((0, registry_git_1.listIgnoredGuardedGitEnvNames)(parent)).toEqual(['GIT_ASKPASS', 'SSH_ASKPASS']);
85
+ expect((0, registry_git_1.listIgnoredGuardedGitEnvNames)(parent)).not.toContain('GIT_EDITOR');
86
+ });
87
+ });
88
+ describe('classifyRegistryGitError (#238 S2 / SG-6)', () => {
89
+ it('maps terminal prompts disabled → authentication with credential.helper remedy', () => {
90
+ const c = (0, registry_git_errors_1.classifyRegistryGitError)(new Error('fatal: could not read Username for \'https://example.com\': terminal prompts disabled'));
91
+ expect(c.class).toBe('authentication');
92
+ expect(c.fatalLine).toMatch(/terminal prompts disabled/i);
93
+ expect(c.remedy).toMatch(/credential\.helper/);
94
+ });
95
+ it('maps TLS / certificate wording → tls', () => {
96
+ const c = (0, registry_git_errors_1.classifyRegistryGitError)(new Error('fatal: unable to access \'https://x/\': SSL certificate problem: unable to get local issuer certificate'));
97
+ expect(c.class).toBe('tls');
98
+ expect(c.remedy).toMatch(/http\.sslCAInfo|http\.sslCAPath/);
99
+ });
100
+ it('maps Could not resolve host → network', () => {
101
+ const c = (0, registry_git_errors_1.classifyRegistryGitError)(new Error('fatal: unable to access \'https://no.such.host/\': Could not resolve host: no.such.host'));
102
+ expect(c.class).toBe('network');
103
+ });
104
+ it('maps timeout plugin message → timeout', () => {
105
+ const c = (0, registry_git_errors_1.classifyRegistryGitError)(new Error('block timeout reached (60000ms) — killing process'));
106
+ expect(c.class).toBe('timeout');
107
+ expect(c.remedy).toMatch(/60s/);
108
+ });
109
+ it('maps git/curl Connection timed out → network, not awm idle timeout (M1)', () => {
110
+ const c = (0, registry_git_errors_1.classifyRegistryGitError)(new Error("fatal: unable to access 'https://github.com/x/y.git/': Failed to connect to github.com port 443 after 133030 ms: Connection timed out"));
111
+ expect(c.class).toBe('network');
112
+ expect(c.remedy).not.toMatch(/killed after 60s/);
113
+ });
114
+ it('maps ssh Could not resolve hostname → network, not ssh (M1)', () => {
115
+ const c = (0, registry_git_errors_1.classifyRegistryGitError)(new Error('ssh: Could not resolve hostname example.invalid: Name or service not known'));
116
+ expect(c.class).toBe('network');
117
+ });
118
+ it('maps SSH host key failures → ssh with core.sshCommand remedy', () => {
119
+ const c = (0, registry_git_errors_1.classifyRegistryGitError)(new Error('fatal: Host key verification failed.'));
120
+ expect(c.class).toBe('ssh');
121
+ expect(c.remedy).toMatch(/core\.sshCommand|~\/\.ssh\/config/);
122
+ });
123
+ it('maps repository not found → not-found', () => {
124
+ const c = (0, registry_git_errors_1.classifyRegistryGitError)(new Error('remote: Repository not found.\nfatal: repository \'https://github.com/x/y.git\' not found'));
125
+ expect(c.class).toBe('not-found');
126
+ expect(c.fatalLine).toMatch(/not found/i);
127
+ });
128
+ it('prefers not-found over broad SSH wording when remote says Repository not found', () => {
129
+ const c = (0, registry_git_errors_1.classifyRegistryGitError)(new Error('ERROR: Repository not found.\nfatal: Could not read from remote repository.'));
130
+ expect(c.class).toBe('not-found');
131
+ expect(c.class).not.toBe('ssh');
132
+ });
133
+ it('maps unknown transport wording → network (default)', () => {
134
+ const c = (0, registry_git_errors_1.classifyRegistryGitError)(new Error('something unexpected happened in git'));
135
+ expect(c.class).toBe('network');
136
+ });
137
+ });
138
+ describe('formatClassifiedRegistryFailure redacts URL credentials (#238 QA B1)', () => {
139
+ it('strips https userinfo from remote= and fatal lines', () => {
140
+ const token = 'ghp_SECRETTOKEN1234567890';
141
+ const remote = `https://${token}@github.com/org/private-reg.git`;
142
+ const msg = (0, registry_git_errors_1.formatClassifiedRegistryFailure)({
143
+ name: 'corp',
144
+ remote,
145
+ err: new Error(`fatal: repository '${remote}' not found`),
146
+ });
147
+ expect(msg).toContain('github.com/org/private-reg.git');
148
+ expect(msg).not.toContain(token);
149
+ expect(msg).not.toMatch(/ghp_/);
150
+ expect(msg).toMatch(/class=not-found/);
151
+ expect(msg).toMatch(/remedy:/);
152
+ });
153
+ });
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.commitSeededRegistry = commitSeededRegistry;
4
+ // Local `.git` commit for hand-seeded registry content roots used by real
5
+ // (unstubbed) `awm init` / `stepCache` paths.
6
+ //
7
+ // Under SG-7 (#238), any sync/clone error hard-fails init. Diagnostics treat
8
+ // `registryCache.present` as "`.git` exists" (`context.ts`), so a content-only
9
+ // fixture always triggers sync against the unreachable `example.invalid`
10
+ // remote. Committing locally makes cache present and lets stepCache skip.
11
+ const child_process_1 = require("child_process");
12
+ function commitSeededRegistry(root) {
13
+ (0, child_process_1.execFileSync)('git', ['init', '-q'], { cwd: root, stdio: 'ignore' });
14
+ (0, child_process_1.execFileSync)('git', ['config', 'user.email', 'fixture@example.invalid'], { cwd: root, stdio: 'ignore' });
15
+ (0, child_process_1.execFileSync)('git', ['config', 'user.name', 'fixture'], { cwd: root, stdio: 'ignore' });
16
+ (0, child_process_1.execFileSync)('git', ['add', '-A'], { cwd: root, stdio: 'ignore' });
17
+ (0, child_process_1.execFileSync)('git', ['commit', '-qm', 'fixture'], { cwd: root, stdio: 'ignore' });
18
+ }
@@ -42,6 +42,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
42
42
  const fs_1 = __importDefault(require("fs"));
43
43
  const os_1 = __importDefault(require("os"));
44
44
  const path_1 = __importDefault(require("path"));
45
+ const commit_seeded_registry_1 = require("../helpers/commit-seeded-registry");
45
46
  describe('codex provider — isolated home E2E (Task 9)', () => {
46
47
  let tmpHome;
47
48
  let tmpWork;
@@ -79,8 +80,10 @@ describe('codex provider — isolated home E2E (Task 9)', () => {
79
80
  });
80
81
  /** Hand-seeds a registry content root with everything a real `awm init` needs:
81
82
  * hooks (Claude + Codex), the bootstrap skill, one baseline bundle providing
82
- * both a skill and a Codex-native agent artifact. No git repo involved — the
83
- * registry cache is content-root-shaped, exactly what a real clone produces. */
83
+ * both a skill and a Codex-native agent artifact. Content-root-shaped like a
84
+ * real clone, plus a local `.git` commit so `machine.registryCache.present`
85
+ * is true and `stepCache` skips network sync (SG-7 hard-fails any sync error,
86
+ * and `https://example.invalid/...` is intentionally unreachable). */
84
87
  function seedPublicRegistryFixture(root) {
85
88
  fs_1.default.mkdirSync(path_1.default.join(root, 'hooks'), { recursive: true });
86
89
  fs_1.default.writeFileSync(path_1.default.join(root, 'hooks/session-start'), '#!/bin/sh\necho "{}"\n', { mode: 0o755 });
@@ -101,6 +104,8 @@ describe('codex provider — isolated home E2E (Task 9)', () => {
101
104
  name: 'dev-core', description: '', version: '1.0.0', scope: 'baseline', visibility: 'public',
102
105
  dependsOn: [], skills: ['development-process'], workflows: [], agents: ['development-process'],
103
106
  }));
107
+ // Local commit only — no origin. Present+.git makes stepCache skip sync.
108
+ (0, commit_seeded_registry_1.commitSeededRegistry)(root);
104
109
  fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm'), { recursive: true });
105
110
  fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.awm/registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.invalid/baseline.git' }], null, 2));
106
111
  }
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  const fs_1 = __importDefault(require("fs"));
37
37
  const os_1 = __importDefault(require("os"));
38
38
  const path_1 = __importDefault(require("path"));
39
+ const commit_seeded_registry_1 = require("../helpers/commit-seeded-registry");
39
40
  describe('copilot provider — isolated home E2E (devCore global-scope guard regression)', () => {
40
41
  let tmpHome;
41
42
  let tmpWork;
@@ -67,10 +68,11 @@ describe('copilot provider — isolated home E2E (devCore global-scope guard reg
67
68
  process.env.AWM_HOME = originalAwmHome;
68
69
  });
69
70
  /** Same fixture shape as codex-provider-isolated.test.ts's seedPublicRegistryFixture:
70
- * a real content-root registry (no git repo needed) with the bootstrap skill and one
71
- * baseline bundle. Copilot needs no hooks (providers/index.ts has no `hooks` config
72
- * for it) and no native agent renderer (`agent: null`), so the fixture is intentionally
73
- * smaller than Codex's. */
71
+ * a real content-root registry with the bootstrap skill and one baseline bundle,
72
+ * plus a local `.git` commit so SG-7 does not hard-fail init on an unreachable
73
+ * example.invalid remote (stepCache skips when registryCache.present). Copilot
74
+ * needs no hooks (providers/index.ts has no `hooks` config for it) and no native
75
+ * agent renderer (`agent: null`), so the fixture is intentionally smaller than Codex's. */
74
76
  function seedPublicRegistryFixture(root) {
75
77
  fs_1.default.mkdirSync(path_1.default.join(root, 'hooks'), { recursive: true });
76
78
  fs_1.default.writeFileSync(path_1.default.join(root, 'hooks/session-start'), '#!/bin/sh\necho "{}"\n', { mode: 0o755 });
@@ -88,6 +90,7 @@ describe('copilot provider — isolated home E2E (devCore global-scope guard reg
88
90
  name: 'dev-core', description: '', version: '1.0.0', scope: 'baseline', visibility: 'public',
89
91
  dependsOn: [], skills: ['development-process'], workflows: [], agents: [],
90
92
  }));
93
+ (0, commit_seeded_registry_1.commitSeededRegistry)(root);
91
94
  fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm'), { recursive: true });
92
95
  fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.awm/registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.invalid/baseline.git' }], null, 2));
93
96
  }